From 804357b212ff02336766633be36a35bcb1641343 Mon Sep 17 00:00:00 2001 From: daxcay Date: Thu, 28 Nov 2024 17:51:53 +0530 Subject: [PATCH] Comfy Preview Update Bug --- .gitignore | 2 +- web/lib/nexus.api.js | 115 +++++ web/lib/nexus.chat.js | 160 +++++++ web/lib/nexus.cmd.js | 737 ++++++++++++++++++++++++++++++++ web/lib/nexus.comfy.js | 261 +++++++++++ web/lib/nexus.elements.js | 217 ++++++++++ web/lib/nexus.litegraph.js | 693 ++++++++++++++++++++++++++++++ web/lib/nexus.mouse.js | 190 ++++++++ web/lib/nexus.panel.js | 258 +++++++++++ web/lib/nexus.sockets.js | 135 ++++++ web/lib/nexus.styles.js | 37 ++ web/lib/nexus.user.manager.js | 27 ++ web/lib/nexus.workflow.js | 306 +++++++++++++ web/lib/nexus.workflow.panel.js | 236 ++++++++++ web/util.js | 32 +- 15 files changed, 3396 insertions(+), 10 deletions(-) create mode 100644 web/lib/nexus.api.js create mode 100644 web/lib/nexus.chat.js create mode 100644 web/lib/nexus.cmd.js create mode 100644 web/lib/nexus.comfy.js create mode 100644 web/lib/nexus.elements.js create mode 100644 web/lib/nexus.litegraph.js create mode 100644 web/lib/nexus.mouse.js create mode 100644 web/lib/nexus.panel.js create mode 100644 web/lib/nexus.sockets.js create mode 100644 web/lib/nexus.styles.js create mode 100644 web/lib/nexus.user.manager.js create mode 100644 web/lib/nexus.workflow.js create mode 100644 web/lib/nexus.workflow.panel.js diff --git a/.gitignore b/.gitignore index c9e9120..db75471 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,7 @@ develop-eggs/ downloads/ eggs/ .eggs/ -lib/ +# lib/ lib64/ parts/ sdist/ diff --git a/web/lib/nexus.api.js b/web/lib/nexus.api.js new file mode 100644 index 0000000..93c7da0 --- /dev/null +++ b/web/lib/nexus.api.js @@ -0,0 +1,115 @@ +const BASE_URL = 'nexus'; + +async function fetchWithHandling(url, options = {}) { + try { +// console.info("Calling: ", url) + const response = await fetch(url, options); + const data = await response.json(); + if (!response.ok) { + throw new Error(data.error || 'Something went wrong'); + } + return data; + } catch (error) { + console.error(`Fetch error: ${error.message}`); + return { error: error.message }; + } +} + +export async function getUserSpecificWorkflows(userId) { + const url = `${BASE_URL}/workflows`; + const options = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ user_id: userId }), + }; + return await fetchWithHandling(url, options); +} + +export async function getUserSpecificWorkflow(userId, workflowId) { + const url = `${BASE_URL}/workflows/${workflowId}`; + const options = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ user_id: userId }), + }; + return await fetchWithHandling(url, options); +} + +export async function getUserSpecificWorkflowsMeta(userId) { + const url = `${BASE_URL}/workflows/meta`; + const options = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ user_id: userId }), + }; + return await fetchWithHandling(url, options); +} + +export async function postWorkflow(userId, workflowData) { + const url = `${BASE_URL}/workflow`; + const options = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ user_id: userId, ...workflowData }), + }; + return await fetchWithHandling(url, options); +} + +export async function postBackupWorkflow(userId, workflowData) { + const url = `${BASE_URL}/workflow/backup`; + const options = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ user_id: userId, ...workflowData }), + }; + return await fetchWithHandling(url, options); +} + +export async function getLatestWorkflow() { + const url = `${BASE_URL}/workflow`; + return await fetchWithHandling(url); +} + +export async function getPermissionById(userId) { + const url = `${BASE_URL}/permission/${userId}`; + return await fetchWithHandling(url); +} + +export async function postPermissionById(userId, adminId, data, token) { + const url = `${BASE_URL}/permission/${userId}`; + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': token, + }, + body: JSON.stringify({ admin_id: adminId, data }), + }; + return await fetchWithHandling(url, options); +} + +export async function nexusLogin(uuid, account, password) { + const url = `${BASE_URL}/login`; + const options = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ uuid, account, password }), + }; + return await fetchWithHandling(url, options); +} + +export async function nexusVerify(token) { + const url = `${BASE_URL}/verify`; + const options = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token }), + }; + return await fetchWithHandling(url, options); +} + +export async function getNameById(userId) { + const url = `${BASE_URL}/name/${userId}`; + return await fetchWithHandling(url); +} + diff --git a/web/lib/nexus.chat.js b/web/lib/nexus.chat.js new file mode 100644 index 0000000..cf21bc5 --- /dev/null +++ b/web/lib/nexus.chat.js @@ -0,0 +1,160 @@ +import { DynamicElement } from "./nexus.elements.js" +import { StyleInjector } from "./nexus.styles.js"; + +let chatMain = new DynamicElement({ + id: 'nexus-chat-main', + tag: 'div', + visible: true, + styles: { + width: '512px', + minHeight: '170px', + maxHeight: '170px', + overflow: 'hidden', + padding: '16px', + borderRadius: '8px', + border: '1px solid rgba(94,94,94,0.5)' + }, + hoverStyles: { + overflowY: 'scroll' + }, + afterChildrenAppend: function (childrens, me) { + if (childrens.length > 50) { + childrens[0].removeElement() + childrens.shift() + } + me.el.scrollTop = me.el.scrollHeight + } +}) + +let inputMain = new DynamicElement({ + id: 'nexus-input-main', + tag: 'div', + visible: true, + styles: { + width: '100%', + height: '32px', + display: 'flex', + alignItems: 'center', + }, + childrens: [ + new DynamicElement({ + id: 'nexus-input-label', + tag: 'span', + visible: false, + styles: { + fontFamily: 'Calibri, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', + display: 'block', + fontSize: '14px', + lineHeight: '14px', + fontWeight: 600, + color: 'white', + padding: '0', + margin: '0', + textShadow: '1pt 1pt #000', + }, + childrens: ['Say:'] + }), + new DynamicElement({ + id: 'nexus-input', + tag: 'input', + visible: false, + styles: { + fontFamily: 'Calibri, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', + fontSize: '14px', + lineHeight: '14px', + fontWeight: 600, + width: '100%', + color: 'white', + padding: '0', + margin: '0', + border: 'none', + background: "transparent", + flexGrow: 1, + outline: 'none', + textIndent: '4pt', + caretColor: 'transparent', + textShadow: '1pt 1pt #000', + } + }) + ] +}) + +let chatWindow = new DynamicElement({ + id: 'nexus-chat-window', + tag: 'div', + styles: { + position: 'absolute', + left: "16px", + top: '100px', + display: 'flex', + flexDirection: 'column', + zIndex: 1000 + }, + childrens: [ + new DynamicElement({ + id: "nexus-total-players", + tag: "div", + visible: true, + styles: { + fontFamily: 'Calibri, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', + fontSize: '14px', + lineHeight: '14px', + fontWeight: 600, + left: "16px", + top: '80px', + color: "#a5a5a5", + width: "100%", + paddingBottom: "12px", + textAlign: "right" + }, + childrens: ['Users Online: ', new DynamicElement({ id: "nexus-total-players-count", visible: true, tag: "span", childrens:['0'] })] + }), + chatMain, + inputMain, + new StyleInjector({ + styles: { + '#nexus-chat-main::-webkit-scrollbar': { + 'display': 'none', + 'scrollbar-width': 'none', + '-ms-overflow-style': 'none', + }, + "#nexus-chat-window" : { + // for pointer events + } + } + }) + ] +}) + +let addChatWindowMessage = function (subject, message, subject_color, message_color, merge) { + let msg = "" + if (merge) { + msg = `${subject} ${message}` + } + else { + msg = `${subject}: ${message}` + } + + let chatElement = new DynamicElement({ + tag: 'span', + visible: true, + styles: { + fontFamily: 'Calibri, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', + display: 'block', + fontSize: '14px', + lineHeight: '14px', + fontWeight: 600, + width: '100%', + color: 'white', + padding: '0', + margin: '0', + textShadow: '1pt 1pt #000', + marginBottom: '1pt', + lineBreak: 'anywhere' + }, + childrens: [msg], + }) + chatMain.addChild(chatElement) +} + +export { chatMain, inputMain, chatWindow, addChatWindowMessage } \ No newline at end of file diff --git a/web/lib/nexus.cmd.js b/web/lib/nexus.cmd.js new file mode 100644 index 0000000..7d1eeb0 --- /dev/null +++ b/web/lib/nexus.cmd.js @@ -0,0 +1,737 @@ +import { nexusLogin, nexusVerify, getPermissionById, postPermissionById, getUserSpecificWorkflowsMeta, getUserSpecificWorkflow } from "./nexus.api.js" +import { addChatWindowMessage, inputMain, chatWindow } from "./nexus.chat.js" +import { UserPanel } from "./nexus.panel.js" +import { WorkflowPanel } from "./nexus.workflow.panel.js" + +export class NexusCommands { + + constructor(app, api, nexusSocket, color, mouseManger, workflowManager, promptControl) { + + this.color = color + + this.colors = { + "join": "#ff6e4d", + "info": "#fba510", + "personal": "#fba510", + "name": "#00fa00", + "none": "#ffffff" + } + + this.inputMain = inputMain + this.nexusSocket = nexusSocket + + this.app = app + this.api = api + + this.workflowManager = workflowManager + this.promptControl = promptControl + this.mouseManger = mouseManger + + this.nexusSocket.onConnected.push(this.handleConnect.bind(this)) + this.nexusSocket.onMessageReceive.push(this.handleMessageReceive.bind(this)) + + this.inputMain.addEventListener('message', this.handleMessage.bind(this)) + + document.addEventListener('keydown', this.handleChatWindow.bind(this)); + + this.disableComfyThings = this.disableComfyThings.bind(this); + this.enableComfyThings = this.enableComfyThings.bind(this); + this.disableGraphSelectionWithExtremePrejudice = this.disableGraphSelectionWithExtremePrejudice.bind(this); + + this.comfyThings = [ + '#extraOptions', + '#queue-button', + '#comfy-save-button', + '#comfy-file-input', + '#comfy-clear-button', + '#comfy-load-default-button', + '.queue-tab-button.side-bar-button', + '.queue-tab-button.side-bar-button', + '#comfy-refresh-button', + '#queue-front-button', + '#comfy-view-queue-button', + '#comfy-view-history-button' + ]; + + chatWindow.childrens[3].softUpdate('#nexus-chat-window', 'pointer-events', 'none') + chatWindow.show() + + setInterval((chatWindow) => { + chatWindow.childrens[0].childrens[1].el.textContent = Object.keys(this.nexusSocket.userManager.users).length + }, 1000, chatWindow); + + this.up = new UserPanel() + this.up.onCommandReceived = this.onPanelCommandReceived.bind(this) + + this.wp = new WorkflowPanel() + this.wp.onCommandReceived = this.onWFPanelCommandReceived.bind(this) + + this.spectating = null + this.lastkey = null + + } + + timeAgo(timeString) { + const now = new Date(); // current time + const time = new Date(timeString); // input time + + const diff = Math.floor((now - time) / 1000); // difference in seconds + + const minutes = Math.floor(diff / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (minutes < 1) return "just now"; + if (minutes === 1) return "1 minute ago"; + if (minutes < 60) return `${minutes} minutes ago`; + if (hours === 1) return "1 hour ago"; + if (hours < 24) return `${hours} hours ago`; + if (days === 1) return "1 day ago"; + + return `${days} days ago`; + } + + async verifyLogin() { + + this.disableComfyThings() + + let token = localStorage.getItem('nexus-socket-token'); + if (token) { + let user = await nexusVerify(token) + if (user && this.nexusSocket.uuid == user.user_id) { + this.enableComfyThings() + this.promptControl.handleControl(true) + this.promptControl.handleEditor(true) + this.workflowManager.handleWorkflowEvents(true) + this.nexusSocket.admin = true + addChatWindowMessage("", "You are now an admin!", this.color.info, this.colors.personal, true) + this.workflowManager.saveManager(60) + } + else { + this.promptControl.handleControl(false) + this.promptControl.handleEditor(false) + this.workflowManager.handleWorkflowEvents(false) + this.workflowManager.removeSaveManager() + addChatWindowMessage("", "You are a viewer!", this.color.info, this.colors.info, true) + } + } + else { + + let permissions = await getPermissionById(this.nexusSocket.uuid) + + if (permissions) { + + let editor = permissions.editor ? permissions.editor : false + let queue = permissions.queue ? permissions.queue : false + + this.workflowManager.handleWorkflowEvents(editor) + + this.promptControl.handleEditor(editor) + this.promptControl.handleControl(queue) + + this.nexusSocket.queue = queue + this.nexusSocket.editor = editor + + if (editor) { + this.workflowManager.saveManager(60) + } + else { + this.workflowManager.removeSaveManager() + } + + addChatWindowMessage("", `Permission: Workflow Editor ${editor ? "Yes" : "No"} | Queue Prompt ${queue ? "Yes" : "No"} `, this.color.info, this.colors.personal, true) + + if (queue) { + addChatWindowMessage("", `You can now queue prompt press CTRL+Enter`, this.color.info, this.colors.personal, true) + } + + } + + } + + this.workflowManager.loadLatestWorkflow() + } + + handleConnect() { + + let name = this.nexusSocket.userManager.get(this.nexusSocket.uuid, "name") + this.nexusSocket.sendMessage('chat-join', { name }, false) + this.verifyLogin() + + } + + findChildrenAfterHr(parentClass) { + const parentDiv = document.querySelector(`.${parentClass}`); + const childElementsAfterHr = []; + if (parentDiv) { + const hr = parentDiv.querySelector('hr'); + if (hr) { + let sibling = hr.nextElementSibling; + while (sibling) { + childElementsAfterHr.push(sibling); + sibling = sibling.nextElementSibling; + } + } + } + return childElementsAfterHr; + } + + disableGraphSelectionWithExtremePrejudice() { + if (!this.nexusSocket.admin && !this.nexusSocket.editor) { + if (this.app.graph && this.app.graph.list_of_graphcanvas && this.app.graph.list_of_graphcanvas.length > 0) { + this.app.graph.list_of_graphcanvas[0].deselectAllNodes() + } + } + } + + disableComfyThings() { + + this.comfyThings.forEach((selector) => { + const elements = document.querySelectorAll(selector); + elements.forEach((element) => { + element.disabled = true; + element.style.pointerEvents = 'none'; + }); + }); + + document.querySelectorAll('.comfy-settings-btn').forEach(button => { + button.disabled = true; + button.style.pointerEvents = 'none'; + }); + + const children = this.findChildrenAfterHr('comfy-menu'); + + children.forEach(element => { + if (element.tagName === 'BUTTON' || element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA') { + element.disabled = true; + } + }); + + document.querySelector(`.comfy-menu`).style.display = 'none'; + document.querySelector(`.comfy-menu`).style.pointerEvents = 'none'; + + + clearInterval(this.disableComfyThingsTimer) + + this.disableComfyThingsTimer = setInterval(() => { + + + this.comfyThings.forEach((selector) => { + const elements = document.querySelectorAll(selector); + elements.forEach((element) => { + element.disabled = true; + element.style.pointerEvents = 'none'; + }); + }); + + document.querySelectorAll('.comfy-settings-btn').forEach(button => { + button.disabled = true; + button.style.pointerEvents = 'none'; + }); + + const children = this.findChildrenAfterHr('comfy-menu'); + + children.forEach(element => { + if (element.tagName === 'BUTTON' || element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA') { + element.disabled = true; + } + }); + + document.querySelector(`.comfy-menu`).style.display = 'none'; + document.querySelector(`.comfy-menu`).style.pointerEvents = 'none'; + + this.promptControl.handleEditor(this.nexusSocket.editor ? this.nexusSocket.editor : false) + this.promptControl.handleControl(this.nexusSocket.queue ? this.nexusSocket.queue : false) + + }, 500); + + this.disableGraphSelectionWithExtremePrejudiceTimer = setInterval(this.disableGraphSelectionWithExtremePrejudice, 50) + + } + + enableComfyThings() { + + clearInterval(this.disableComfyThingsTimer) + clearInterval(this.disableGraphSelectionWithExtremePrejudiceTimer) + + this.comfyThings.forEach((selector) => { + const elements = document.querySelectorAll(selector); + elements.forEach((element) => { + element.disabled = false; + element.style.pointerEvents = 'all'; + }); + }); + + document.querySelectorAll('.comfy-settings-btn').forEach(button => { + button.disabled = false; + button.style.pointerEvents = 'all'; + }); + + const children = this.findChildrenAfterHr('comfy-menu'); + + children.forEach(element => { + if (element.tagName === 'BUTTON' || element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA') { + element.disabled = false; + } + }); + + document.querySelector(`.comfy-menu`).style.display = 'flex'; + document.querySelector(`.comfy-menu`).style.pointerEvents = 'all'; + + } + + + handleChatWindow(event) { + + let key = event.key + let label = inputMain.childrens[0] + let input = inputMain.childrens[1] + + let active = { + tag: document.activeElement.tagName, + el: document.activeElement, + chat: document.activeElement == input.el + } + + function activateChat() { + let allowedTags = ['INPUT', 'TEXTAREA'] + if (!allowedTags.includes(active.tag) && input.visible == false) { + event.preventDefault(); + label.show() + input.show() + input.el.focus() + input.el.value = "" + input.focusTimer = setInterval((parent, input) => { + if (document.activeElement != input && parent.visible == true) + input.focus() + }, 500, input, input.el); + chatWindow.childrens[3].softUpdate('#nexus-chat-window', 'pointer-events', 'all') + } + } + + function deactivateChat() { + if (!active.chat) return; + label.hide() + input.el.blur(); + input.el.value = "" + input.hide() + clearTimeout(input.focusTimer) + chatWindow.childrens[3].softUpdate('#nexus-chat-window', 'pointer-events', 'none') + } + + function sendMessage() { + if (!active.chat) return; + let message = input.el.value + inputMain.dispatchEvent(new CustomEvent('message', { detail: { message } })) + deactivateChat() + } + + switch (key) { + case 't': + case 'T': + activateChat() + this.togglePlayerTab(true) + this.toggleWFTab(true) + break; + case 'Escape': + deactivateChat() + break; + case 'Enter': + sendMessage() + break; + case 'p': + case 'P': + if (event.altKey && event.shiftKey) { + if (!active.chat) + this.togglePlayerTab() + } + break; + case 'o': + case 'O': + if (event.altKey && event.shiftKey) { + if (!active.chat) + this.toggleWFTab() + } + break; + + } + + return active + } + + toggleWFTab(forceHide, forceReload) { + if (this.nexusSocket.admin || this.nexusSocket.editor) { + + if (forceHide) { + this.wp.active = 0 + this.wp.panel.hide() + } else if (forceReload) { + this.prepareWFPanel() + } + else { + if (this.wp.active == 0) { + this.wp.active = 1 + this.wp.panel.show() + this.prepareWFPanel() + } else if (this.wp.active == 1) { + this.wp.active = 0 + this.wp.panel.hide() + } + } + + if (this.wp.active == 1) { + this.workflowManager.removeSaveManager() + } + else { + this.workflowManager.saveManager(this.workflowManager.saveTime || 60) + } + + } + } + + togglePlayerTab(forceHide, forceReload) { + + if (forceHide) { + this.up.active = 0 + this.up.panel.hide() + } else if (forceReload) { + this.preparePanel(this.nexusSocket.admin) + } else { + if (this.up.active == 0) { + this.up.active = 1 + this.up.panel.show() + this.preparePanel(this.nexusSocket.admin) + } else if (this.up.active == 1) { + this.up.active = 0 + this.up.panel.hide() + } + } + + } + + async onWFPanelCommandReceived(kind, label, data, active) { + + switch (kind) { + case 'button': + if (label == "load") { + let wfid = data // workflowid is data + let wf = await getUserSpecificWorkflow(this.nexusSocket.uuid, wfid) + if (wf) { + wf.clear = true + let message = { + name: "workflow", + from: this.nexusSocket.uuid, + data: { workflow: wf } + } + this.nexusSocket.onMessageReceive.forEach(f => { f(message); }); + if (this.nexusSocket.admin) { + this.nexusSocket.sendMessage('workflow', { workflow: wf }, false) + } + } + } + break; + } + + } + + async onPanelCommandReceived(kind, label, data, active) { + + switch (kind) { + case 'button': + if (label == "spectate") { + this.nexusSocket.sendMessage("spectate", { on: active, receiver: data }, false) + if (active == 1) { + this.spectating = data // userid is data + } + else { + this.spectating = null + } + } else if (label == "editor") { + if (this.nexusSocket.admin) { + let token = localStorage.getItem('nexus-socket-token'); + await postPermissionById(data, this.nexusSocket.uuid, { editor: active ? true : false }, token) // userid is data + this.nexusSocket.sendMessage("permission", { receiver: data }, false) + } + } else if (label == "queue") { + if (this.nexusSocket.admin) { + let token = localStorage.getItem('nexus-socket-token'); + await postPermissionById(data, this.nexusSocket.uuid, { queue: active ? true : false }, token) // userid is data + this.nexusSocket.sendMessage("permission", { receiver: data }, false) + } + } + else if (label == "mouse") { + this.nexusSocket.sendMessage("mouse_view_toggle", { on: active, receiver: data }, false) + this.nexusSocket.userManager.set(data, 'hide_mouse', active) + } + break; + } + + } + + async prepareWFPanel() { + + const columns = ['Name', 'Created', 'Action']; + + this.wp.addColumnsToPanel(columns); + + let meta = await getUserSpecificWorkflowsMeta(this.nexusSocket.uuid) + + const rows = []; + const callbacks = []; + const values = []; + const data = []; + + for (let id = 0; id < meta.length; id++) { + + const file = meta[id]; + + let fn = file.file_name + "" + + let name = (fn.includes('lt') ? 'Long Term ' : 'Short Term ') + // let time = file.last_saved.split(':') + // time = time.join("|") + + // console.log(time) + let time = this.timeAgo(file.last_saved) + + rows.push([name, time, 'button:load']) + callbacks.push([null, null, this.wp.onclick]) + values.push([null, null, '0']) + data.push([null, null, file.file_name]) + + } + + this.wp.addRowsToPanel(rows, callbacks, values, data); + } + + async preparePanel(admin) { + + const columns = ['Name', 'Actions', 'Online']; + this.up.addColumnsToPanel(columns); + + const users = this.nexusSocket.userManager.users; + const usersIds = Object.keys(users); + + const rows = []; + const callbacks = []; + const values = []; + const data = []; + + for (const id of usersIds) { + + const user = users[id]; + const name = id === this.nexusSocket.uuid ? `${user.name} (You)` : user.name; + const self = id === this.nexusSocket.uuid; + + const status = user.status === "online" || self ? '🟢' : '🔴'; + const hide_mouse = user.hide_mouse ? 0 : 1; + const permissions = await getPermissionById(id); + const spectating = id == this.spectating ? 1 : 0 + + callbacks.push([null, self ? 'none' : this.up.onclick, null]); + + let powers = 'button:spectate,button:editor,button:queue,button:mouse' + + if (!admin) { + powers = 'button:spectate,button:mouse' + } + + rows.push([name, self ? 'button:none' : powers, status]); + + let row_values = `${spectating},${permissions.editor ? 1 : 0},${permissions.queue ? 1 : 0},${hide_mouse}` + if (!admin) { + row_values = `${spectating},${hide_mouse}` + } + + values.push([null, row_values, null]); + + let row_data = `${id},${id},${id},${id}` + if (!admin) { + row_data = `${id},${id}` + } + + data.push([null, row_data, null]); + + } + + this.up.addRowsToPanel(rows, callbacks, values, data); + } + + handleMessageReceive(message) { + + let name = message.name; + let from = message.from; + let data = message.data + + // console.log(name, data, from) + + if (name == "chat") { + + let user_name = this.nexusSocket.userManager.get(from, "name") || "User"; + let mouse = this.nexusSocket.userManager.get(from, "mouse") || "#0f0"; + let message = data.message + addChatWindowMessage(user_name, message, mouse[3], this.colors.none) + + } else if (name == "nick") { + + let old_name = data.old_name + let new_name = data.new_name + + addChatWindowMessage(old_name + " is now known as ", new_name, this.colors.personal, this.colors.personal, true) + this.nexusSocket.userManager.set(from, "name", new_name) + + } else if (name == "chat-join") { + + let name = data.name + addChatWindowMessage(name + " joined the server", "", this.colors.personal, this.colors.personal, true) + this.nexusSocket.userManager.set(from, "name", name) + + name = this.nexusSocket.userManager.get(this.nexusSocket.uuid, "name") + this.nexusSocket.sendMessage('name', { name }, false) + + if (this.up.active) { + this.togglePlayerTab(false, true) + } + + } else if (name == "name") { + + let name = data.name + this.nexusSocket.userManager.set(from, "name", name) + //console.log("Received name:", name) + + if (this.up.active) { + this.togglePlayerTab(false, true) + } + + } else if (name == 'afk' || name == 'online') { + + this.nexusSocket.userManager.set(from, "status", name) + + if (this.up.active) { + this.togglePlayerTab(false, true) + } + + } else if (name == 'permission') { + + + // window.location.reload() + + this.verifyLogin() + + if (this.up.active) { + this.togglePlayerTab(false, true) + } + + } + + } + + async handleMessage(evt) { + + let message = evt.detail.message + message = this.cleanMessage(message) + + if (message) { + + let startsWithCommand = this.startsWithCommand(message) + + if (startsWithCommand) { + let parts = this.extractCommandAndValue(message) + let cmds = { 'nick': true, 'login': true, 'logout': true } + let cmd = parts.command + let values = parts.values + + if (cmds[cmd]) { + switch (cmd) { + case 'nick': + let new_name = this.cleanMessage(values[0], 16) + let old_name = this.nexusSocket.userManager.get(this.nexusSocket.uuid, "name") || "User" + if (!new_name) { + addChatWindowMessage("", "Name is invalid", this.colors.info, this.colors.info, true) + } + else { + localStorage.setItem('nexus-socket-name', new_name); + this.nexusSocket.sendMessage('nick', { old_name, new_name }) + } + break; + case 'login': + + let account = this.cleanMessage(values[0], 16) + let password = this.cleanMessage(values[1], 16) + let uuid = this.nexusSocket.uuid + let token = await nexusLogin(uuid, account, password) + if (token) { + localStorage.setItem('nexus-socket-token', token.token); + // addChatWindowMessage("", "You are now an admin!", this.color.info, this.colors.info, true) + this.verifyLogin() + // window.location.reload() + } + else { + addChatWindowMessage("", "Admin account/password invalid!", this.color.info, this.colors.info, true) + } + break; + case 'logout': + let usertoken = localStorage.getItem('nexus-socket-token') + if (this.nexusSocket.admin) { + await postPermissionById(this.nexusSocket.uuid, this.nexusSocket.uuid, { admin: false, queue: false, editor: false }, usertoken) + localStorage.removeItem('nexus-socket-token'); + window.location.reload() + } + break; + + default: + break; + } + } + + } + else { + addChatWindowMessage("You", message, this.color, this.colors.none) + this.nexusSocket.sendMessage('chat', { message }, false) + } + + } + + } + + cleanMessage(message, length = 512) { + message = message.substr(0, length); + if (message.length === 0) { + return null; + } else { + let msg = ""; + const nonAsciiRe = /[^\x00-\x7F]/; + const emojiRe = /[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F700}-\u{1F77F}]|[\u{1F780}-\u{1F7FF}]|[\u{1F800}-\u{1F8FF}]|[\u{1F900}-\u{1F9FF}]|[\u{1FA00}-\u{1FA6F}]|[\u{1FA70}-\u{1FAFF}]/u; + const htmlTagRe = /<\/?[^>]+(>|$)/g; + message = message.replace(htmlTagRe, ''); + for (let char of message) { + if (!(nonAsciiRe.test(char) && !emojiRe.test(char))) { + msg += char; + } + } + return msg; + } + } + + startsWithCommand(str) { + const commandPattern = /^\/\w+(\s+\S+)?/; + return commandPattern.test(str); + } + + extractCommandAndValue(str) { + const commandPattern = /^\/(\w+)\s*(.*)/; + const match = str.match(commandPattern); + if (match) { + const command = match[1]; + const values = match[2].trim() ? match[2].split(/\s+/) : []; + return { + command, + values + }; + } else { + return null; + } + } + + +} \ No newline at end of file diff --git a/web/lib/nexus.comfy.js b/web/lib/nexus.comfy.js new file mode 100644 index 0000000..1a34052 --- /dev/null +++ b/web/lib/nexus.comfy.js @@ -0,0 +1,261 @@ +export class NexusPromptControl extends EventTarget { + + constructor(api, app, nexusSocket) { + + super() + + this.app = app + this.api = api + this.runningNodeId = null + this.nexusSocket = nexusSocket + this.comfy_events = ['b_preview', 'progress', 'executing', 'executed', 'execution_start', 'execution_error', 'execution_cached']; + + let oqp = api.queuePrompt; + this.apiWrapper = { + oqp: oqp, + enableApi: false, + api: api, + qp: async function (number, data) { + if (!this.enableApi) { + return { + "node_errors": {} + }; + } + return this.oqp.call(this.api, number, { output: data.output, workflow: data.workflow }); + } + }; + + this.api.queuePrompt = this.apiWrapper.qp.bind(this.apiWrapper); + this.nexusSocket.onMessageReceive.push(this.handleMessageReceive.bind(this)) + + this.addEventListeners(); + this.comfy_events.forEach((event) => { + this.api.addEventListener(event, this.handleComfyEvents.bind(this)); + }); + + this.handleProgressDraw() + this.handleControl(false) + this.handleEditor(false) + + } + + handleProgressDraw() { + + const origDrawNodeShape = LGraphCanvas.prototype.drawNodeShape; + const self = this.app; + const self2 = this; + + LGraphCanvas.prototype.drawNodeShape = function (node, ctx, size, fgcolor, bgcolor, selected, mouse_over) { + + const res = origDrawNodeShape.apply(this, arguments); + const nodeErrors = self.lastNodeErrors?.[node.id]; + + let color = null; + let lineWidth = 1; + + if (node.id === +self2.runningNodeId) { + color = "#0f0"; + } else if (self.dragOverNode && node.id === self.dragOverNode.id) { + color = "dodgerblue"; + } + else if (nodeErrors?.errors) { + color = "red"; + lineWidth = 2; + } + else if (self.lastExecutionError && self.lastExecutionError.node_id === node.id) { + color = "#f0f"; + lineWidth = 2; + } + + if (color) { + const shape = node._shape || node.constructor.shape || LiteGraph.ROUND_SHAPE; + ctx.lineWidth = lineWidth; + ctx.globalAlpha = 0.8; + ctx.beginPath(); + if (shape == LiteGraph.BOX_SHAPE) + ctx.rect(-6, -6 - LiteGraph.NODE_TITLE_HEIGHT, 12 + size[0] + 1, 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT); + else if (shape == LiteGraph.ROUND_SHAPE || (shape == LiteGraph.CARD_SHAPE && node.flags.collapsed)) + ctx.roundRect( + -6, + -6 - LiteGraph.NODE_TITLE_HEIGHT, + 12 + size[0] + 1, + 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT, + this.round_radius * 2 + ); + else if (shape == LiteGraph.CARD_SHAPE) + ctx.roundRect( + -6, + -6 - LiteGraph.NODE_TITLE_HEIGHT, + 12 + size[0] + 1, + 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT, + [this.round_radius * 2, this.round_radius * 2, 2, 2] + ); + else if (shape == LiteGraph.CIRCLE_SHAPE) + ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5 + 6, 0, Math.PI * 2); + ctx.strokeStyle = color; + ctx.stroke(); + ctx.strokeStyle = fgcolor; + ctx.globalAlpha = 1; + } + + if (self.progress && node.id === +self2.runningNodeId) { + ctx.fillStyle = "green"; + ctx.fillRect(0, 0, size[0] * (self.progress.value / self.progress.max), 6); + ctx.fillStyle = bgcolor; + } + + // Highlight inputs that failed validation + if (nodeErrors) { + ctx.lineWidth = 2; + ctx.strokeStyle = "red"; + for (const error of nodeErrors.errors) { + if (error.extra_info && error.extra_info.input_name) { + const inputIndex = node.findInputSlot(error.extra_info.input_name) + if (inputIndex !== -1) { + let pos = node.getConnectionPos(true, inputIndex); + ctx.beginPath(); + ctx.arc(pos[0] - node.pos[0], pos[1] - node.pos[1], 12, 0, 2 * Math.PI, false) + ctx.stroke(); + } + } + } + } + + return res; + } + + } + + handleMessageReceive(message) { + try { + this.dispatchEvent(new CustomEvent(message.name, { detail: message })); + } catch (e) { + console.error('Error handling message:', e); + } + } + + handleComfyEvents(evt) { + if (evt.type === "b_preview") { + this.blobToBase64(evt.detail).then((result) => { + this.nexusSocket.sendMessage(evt.type, { detail: result }, false) + }); + } else { + this.nexusSocket.sendMessage(evt.type, { detail: evt.detail }, false) + } + } + + addEventListeners() { + + this.addEventListener("progress", ({ detail }) => { + detail = detail.data.detail + this.app.progress = detail; + this.app.graph.setDirtyCanvas(true, false); + }); + + this.addEventListener("executing", ({ detail }) => { + detail = detail.data.detail + this.app.progress = null; + this.runningNodeId = detail; + this.app.graph.setDirtyCanvas(true, false); + delete this.app.nodePreviewImages[this.runningNodeId]; + }); + + this.addEventListener("executed", ({ detail }) => { + detail = detail.data.detail + const output = this.app.nodeOutputs[detail.node]; + if (detail.merge && output) { + for (const k in detail.output ?? {}) { + const v = output[k]; + if (v instanceof Array) { + output[k] = v.concat(detail.output[k]); + } else { + output[k] = detail.output[k]; + } + } + } else { + this.app.nodeOutputs[detail.node] = detail.output; + } + const node = this.app.graph.getNodeById(detail.node); + if (node && node.onExecuted) { + node.onExecuted(detail.output); + } + }); + + this.addEventListener("execution_start", ({ detail }) => { + detail = detail.data.detail + this.runningNodeId = null; + this.app.lastExecutionError = null; + this.app.graph._nodes.forEach((node) => { + if (node.onExecutionStart) { + node.onExecutionStart(); + } + }); + }); + + this.addEventListener("execution_error", ({ detail }) => { + detail = detail.data.detail + this.app.lastExecutionError = detail; + const formattedError = this.formatExecutionError(detail); + this.app.ui.dialog.show(formattedError); + this.app.canvas.draw(true, true); + }); + + this.addEventListener("b_preview", ({ detail }) => { + detail = detail.data.detail + const id = this.runningNodeId; + if (id == null) return; + const blob = this.base64ToBlob(detail); + const blobUrl = URL.createObjectURL(blob); + this.app.nodePreviewImages[id] = [blobUrl]; + }); + } + + blobToBase64(blob) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => { + resolve(reader.result); + }; + reader.onerror = reject; + reader.readAsDataURL(blob); + }); + } + + base64ToBlob(dataUrl) { + const [header, base64] = dataUrl.split(','); + const mime = header.match(/:(.*?);/)[1]; + const byteCharacters = atob(base64); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + return new Blob([byteArray], { type: mime }); + } + + formatExecutionError(error) { + if (error == null) { + return "(unknown error)"; + } + + const traceback = error.traceback.join(""); + const nodeId = error.node_id; + const nodeType = error.node_type; + + return `Error occurred when executing ${nodeType}:\n\n${error.exception_message}\n\n${traceback}`; + } + + handleEditor(oN) { + + this.app.canvas.allow_dragnodes = oN; + this.app.canvas.allow_interaction = oN; + this.app.canvas.allow_reconnect_links = oN; + this.app.canvas.allow_searchbox = oN; + + } + + handleControl(oN) { + this.apiWrapper.enableApi = oN; + } + +} \ No newline at end of file diff --git a/web/lib/nexus.elements.js b/web/lib/nexus.elements.js new file mode 100644 index 0000000..05294c3 --- /dev/null +++ b/web/lib/nexus.elements.js @@ -0,0 +1,217 @@ +import { StyleInjector } from "./nexus.styles.js"; + +export class DynamicElement extends EventTarget { + id = ''; + tag = ''; + styles = {}; + hoverStyles = {}; + focusStyles = {}; + activeStyles = {}; + attrs = {}; + childrens = []; + events = {}; + appendTo = document.body; + visible = false; + afterChildrenAppend = null; + afterCreated = null; + defaultDisplay = 'initial'; + el = null; + + constructor({ + id, + tag, + styles = {}, + hoverStyles = {}, + focusStyles = {}, + activeStyles = {}, + attrs = {}, + childrens = [], + events = {}, + appendTo = document.body, + afterChildrenAppend = null, + afterCreated = null, + visible = false + } = {}) { + + super() + + this.id = id; + this.tag = tag; + this.styles = styles; + this.hoverStyles = hoverStyles; + this.focusStyles = focusStyles; + this.activeStyles = activeStyles; + this.attrs = attrs; + this.childrens = childrens; + this.events = events; + this.appendTo = appendTo; + this.afterChildrenAppend = afterChildrenAppend; + this.afterCreated = afterCreated; + this.visible = visible + + this.createElement(); + } + + createElement() { + if (!this.tag) { + throw new Error('Tag is required to create an element.'); + } + + this.el = document.createElement(this.tag); + if (this.id) this.el.id = this.id; + this.appendTo.appendChild(this.el); + + this.addStyles(); + this.addAttrs(); + this.addChildrens(); + this.addEvents(); + this.addStateStyles(); + + if (!this.visible) { + this.hide() + } + + if (this.afterCreated) { + this.afterCreated(this) + } + } + + removeElement() { + if (!this.el) return; + this.removeAttrs(); + this.removeEvents(); + this.removeStyles(); + this.removeChildrens(); + this.el.remove(); + this.el = null; + } + + updateStyles(styles = {}) { + this.styles = { ...this.styles, ...styles }; + this.addStyles(); + } + + addStyles() { + if (!this.el) return; + Object.entries(this.styles).forEach(([style, value]) => { + if (style === 'display') { + this.defaultDisplay = value; + } + this.el.style[style] = value; + }); + } + + removeStyles() { + if (!this.el) return; + Object.keys(this.styles).forEach(style => { + this.el.style[style] = ''; + }); + this.styles = {}; + } + + updateAttrs(attrs = {}) { + this.attrs = { ...this.attrs, ...attrs }; + this.addAttrs(); + } + + addAttrs() { + if (!this.el) return; + Object.entries(this.attrs).forEach(([attr, value]) => { + this.el.setAttribute(attr, value); + }); + } + + removeAttrs() { + if (!this.el) return; + Object.keys(this.attrs).forEach(attr => { + this.el.removeAttribute(attr); + }); + this.attrs = {}; + } + + addChildrens() { + if (!this.el) return; + this.childrens.forEach(child => { + if (typeof child === 'string') { + this.el.insertAdjacentHTML('beforeend', child); + } else if (child instanceof Node) { + this.el.appendChild(child); + } else if (child instanceof StyleInjector) { + this.el.appendChild(child.el); + } else if (child instanceof DynamicElement) { + this.el.appendChild(child.el); + } + }); + if (this.afterChildrenAppend) { + requestAnimationFrame(() => this.afterChildrenAppend(this.childrens, this)); + } + } + + addChild(child) { + if (!this.el) return; + if (typeof child === 'string') { + this.el.insertAdjacentHTML('beforeend', child); + } else if (child instanceof Node) { + this.el.appendChild(child); + } else if (child instanceof DynamicElement) { + this.el.appendChild(child.el); + } + if(!this.childrens.includes(child)) { + this.childrens.push(child) + } + if (this.afterChildrenAppend) { + this.afterChildrenAppend(this.childrens, this); + } + } + + removeChildrens() { + if (!this.el) return; + this.el.innerHTML = ''; + this.childrens = []; + } + + addEvents() { + if (!this.el) return; + Object.entries(this.events).forEach(([event, handler]) => { + this.el.addEventListener(event, (e) => { handler(e, this.parent) }); + }); + } + + removeEvents() { + if (!this.el) return; + Object.entries(this.events).forEach(([event, handler]) => { + this.el.removeEventListener(event, handler); + }); + this.events = {}; + } + + show() { + if (!this.el) return; + this.el.style.display = this.defaultDisplay; + this.visible = true; + } + + hide() { + if (!this.el) return; + this.el.style.display = 'none'; + this.visible = false; + } + + addStateStyles() { + if (!this.el) return; + + this.el.addEventListener('mouseenter', () => this.applyStyles(this.hoverStyles)); + this.el.addEventListener('mouseleave', () => this.applyStyles(this.styles)); + this.el.addEventListener('focus', () => this.applyStyles(this.focusStyles), true); + this.el.addEventListener('blur', () => this.applyStyles(this.styles), true); + this.el.addEventListener('mousedown', () => this.applyStyles(this.activeStyles)); + this.el.addEventListener('mouseup', () => this.applyStyles(this.styles)); + } + + applyStyles(stateStyles) { + if (!this.visible) return + Object.entries(stateStyles).forEach(([style, value]) => { + this.el.style[style] = value; + }); + } +} diff --git a/web/lib/nexus.litegraph.js b/web/lib/nexus.litegraph.js new file mode 100644 index 0000000..6231b41 --- /dev/null +++ b/web/lib/nexus.litegraph.js @@ -0,0 +1,693 @@ +LiteGraph.use_uuids = true +LiteGraph.uuidv4 = function() { return Date.now() } + +LGraph.prototype.getGroupByUUID = function (uuid) { + let groups = this._groups + let group = null + for (let index = 0; index < groups.length; index++) { + let tgroup = groups[index]; + if (tgroup.uuid && tgroup.uuid == uuid) { + group = tgroup + break + } + } + return group +} + +LGraph.prototype.getGroupIndexByUUID = function (uuid) { + let groups = this._groups + let at = -1 + for (let index = 0; index < groups.length; index++) { + let tgroup = groups[index]; + if (tgroup.uuid && tgroup.uuid == uuid) { + at = index + break + } + } + return at +} + + +LGraph.prototype.add = function (node, skip_compute_order) { + + if (!node) { + return; + } + + //groups + if (node.constructor === LGraphGroup) { + this._groups.push(node); + this.setDirtyCanvas(true); + this.change(); + if(!node.uuid) { + node.uuid = LiteGraph.uuidv4() + } + node.graph = this; + this._version++; + if (this.onGroupAdded) + this.onGroupAdded(node) + return; + } + + //nodes + if (node.id != -1 && this._nodes_by_id[node.id] != null) { + console.warn( + "Nexus LiteGraph: there is already a node with this ID, will reuse it" + ); + if (LiteGraph.use_uuids) { + node.id = LiteGraph.uuidv4() + } + else { + node.id = ++this.last_node_id; + } + } + + if (this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) { + throw "LiteGraph: max number of nodes in a graph reached"; + } + + //give him an id + if (LiteGraph.use_uuids) { + if (node.id == null || node.id == -1) + node.id = LiteGraph.uuidv4(); + + let uuid = localStorage.getItem('nexus-socket-uuid'); + if (uuid) { + node.properties.owner = uuid + } + this.last_node_id = node.id; + } + else { + if (node.id == null || node.id == -1) { + node.id = ++this.last_node_id; + } else if (this.last_node_id < node.id) { + this.last_node_id = node.id; + } + } + + node.graph = this; + this._version++; + + this._nodes.push(node); + this._nodes_by_id[node.id] = node; + + if (node.onAdded) { + node.onAdded(this); + } + + if (this.config.align_to_grid) { + node.alignToGrid(); + } + + if (!skip_compute_order) { + this.updateExecutionOrder(); + } + + if (this.onNodeAdded) { + this.onNodeAdded(node); + } + + this.setDirtyCanvas(true); + this.change(); + + return node; //to chain actions +}; + +LGraph.prototype.addNexus = function (node, skip_compute_order) { + + if (!node) { + return; + } + + //groups + if (node.constructor === LGraphGroup) { + this._groups.push(node); + this.setDirtyCanvas(true); + this.change(); + // node.id = LiteGraph.uuidv4(); + node.graph = this; + this._version++; + return; + } + + // //nodes + // if (node.id != -1 && this._nodes_by_id[node.id] != null) { + // console.warn( + // "Nexus LiteGraph: there is already a node with this ID, will reuse it" + // ); + // if (LiteGraph.use_uuids) { + // node.id = LiteGraph.uuidv4(); + // } + // else { + // node.id = ++this.last_node_id; + // } + // } + + if (this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) { + throw "LiteGraph: max number of nodes in a graph reached"; + } + + //give him an id + // if (LiteGraph.use_uuids) { + // if (node.id == null || node.id == -1) + // let uuid = localStorage.getItem('nexus-socket-uuid'); + // if (uuid) { + // node.properties.owner = uuid + // } + // node.id = LiteGraph.uuidv4(); + // } + // else { + // if (node.id == null || node.id == -1) { + // node.id = ++this.last_node_id; + // } else if (this.last_node_id < node.id) { + this.last_node_id = node.id; + // } + // } + + node.graph = this; + this._version++; + + this._nodes.push(node); + this._nodes_by_id[node.id] = node; + + if (this.config.align_to_grid) { + node.alignToGrid(); + } + + if (!skip_compute_order) { + this.updateExecutionOrder(); + } + + if (this.onNodeAddedNexus) { + this.onNodeAddedNexus(node); + } + + this.setDirtyCanvas(true); + this.change(); + + return node; //to chain actions +}; + +LGraph.prototype.remove = function (node) { + if (node.constructor === LiteGraph.LGraphGroup) { + var index = this._groups.indexOf(node); + if (index != -1) { + this._groups.splice(index, 1); + } + node.graph = null; + this._version++; + this.setDirtyCanvas(true, true); + this.change(); + if (this.onGroupRemoved) + this.onGroupRemoved(node) + return; + } + + if (this._nodes_by_id[node.id] == null) { + return; + } //not found + + if (node.ignore_remove) { + return; + } //cannot be removed + + this.beforeChange(); //sure? - almost sure is wrong + + //disconnect inputs + if (node.inputs) { + for (var i = 0; i < node.inputs.length; i++) { + var slot = node.inputs[i]; + if (slot.link != null) { + node.disconnectInput(i); + } + } + } + + //disconnect outputs + if (node.outputs) { + for (var i = 0; i < node.outputs.length; i++) { + var slot = node.outputs[i]; + if (slot.links != null && slot.links.length) { + node.disconnectOutput(i); + } + } + } + + //node.id = -1; //why? + + //callback + if (node.onRemoved) { + node.onRemoved(); + } + + node.graph = null; + this._version++; + + //remove from canvas render + if (this.list_of_graphcanvas) { + for (var i = 0; i < this.list_of_graphcanvas.length; ++i) { + var canvas = this.list_of_graphcanvas[i]; + if (canvas.selected_nodes[node.id]) { + delete canvas.selected_nodes[node.id]; + } + if (canvas.node_dragged == node) { + canvas.node_dragged = null; + } + } + } + + //remove from containers + var pos = this._nodes.indexOf(node); + if (pos != -1) { + this._nodes.splice(pos, 1); + } + delete this._nodes_by_id[node.id]; + + if (this.onNodeRemoved) { + this.onNodeRemoved(node); + } + + //close panels + this.sendActionToCanvas("checkPanels"); + + this.setDirtyCanvas(true, true); + this.afterChange(); //sure? - almost sure is wrong + this.change(); + + this.updateExecutionOrder(); +}; + +LGraph.prototype.removeNexus = function (node) { + + if (node.constructor === LiteGraph.LGraphGroup) { + var index = this._groups.indexOf(node); + if (index != -1) { + this._groups.splice(index, 1); + } + node.graph = null; + this._version++; + this.setDirtyCanvas(true, true); + this.change(); + return; + } + + if (this._nodes_by_id[node.id] == null) { + return; + } //not found + + if (node.ignore_remove) { + return; + } //cannot be removed + + this.beforeChange(); //sure? - almost sure is wrong + + //disconnect inputs + if (node.inputs) { + for (var i = 0; i < node.inputs.length; i++) { + var slot = node.inputs[i]; + if (slot.link != null) { + node.disconnectInput(i); + } + } + } + + //disconnect outputs + if (node.outputs) { + for (var i = 0; i < node.outputs.length; i++) { + var slot = node.outputs[i]; + if (slot.links != null && slot.links.length) { + node.disconnectOutput(i); + } + } + } + + //node.id = -1; //why? + + //callback + if (node.onRemoved) { + node.onRemoved(); + } + + node.graph = null; + this._version++; + + //remove from canvas render + if (this.list_of_graphcanvas) { + for (var i = 0; i < this.list_of_graphcanvas.length; ++i) { + var canvas = this.list_of_graphcanvas[i]; + if (canvas.selected_nodes[node.id]) { + delete canvas.selected_nodes[node.id]; + } + if (canvas.node_dragged == node) { + canvas.node_dragged = null; + } + } + } + + //remove from containers + var pos = this._nodes.indexOf(node); + if (pos != -1) { + this._nodes.splice(pos, 1); + } + delete this._nodes_by_id[node.id]; + + //close panels + this.sendActionToCanvas("checkPanels"); + + this.setDirtyCanvas(true, true); + this.afterChange(); //sure? - almost sure is wrong + this.change(); + + this.updateExecutionOrder(); +}; + +LGraphNode.prototype.configureNexus = function (info) { + if (this.graph) { + this.graph._version++; + } + for (var j in info) { + if (j == "properties") { + //i don't want to clone properties, I want to reuse the old container + for (var k in info.properties) { + this.properties[k] = info.properties[k]; + } + continue; + } + + if (info[j] == null) { + continue; + } else if (typeof info[j] == "object") { + //object + if (this[j] && this[j].configure) { + this[j].configure(info[j]); + } else { + this[j] = LiteGraph.cloneObject(info[j], this[j]); + } + } //value + else { + this[j] = info[j]; + } + } + + if (!info.title) { + this.title = this.constructor.title; + } + + if (this.inputs) { + for (var i = 0; i < this.inputs.length; ++i) { + var input = this.inputs[i]; + if (this.onInputAdded) + this.onInputAdded(input); + } + } + + if (this.outputs) { + for (var i = 0; i < this.outputs.length; ++i) { + var output = this.outputs[i]; + if (!output.links) { + continue; + } + if (this.onOutputAdded) + this.onOutputAdded(output); + } + } + + if (this.widgets) { + for (var i = 0; i < this.widgets.length; ++i) { + var w = this.widgets[i]; + if (!w) + continue; + if (w.options && w.options.property && (this.properties[w.options.property] != undefined)) + w.value = JSON.parse(JSON.stringify(this.properties[w.options.property])); + } + if (info.widgets_values) { + for (var i = 0; i < info.widgets_values.length; ++i) { + if (this.widgets[i]) { + this.widgets[i].value = info.widgets_values[i]; + } + } + } + } + + if (this.onConfigure) { + this.onConfigure(info); + } +}; + +LGraphCanvas.prototype.selectNodesNexus = function (nodes, add_to_current_selection) { + if (!add_to_current_selection) { + this.deselectAllNodes(); + } + + nodes = nodes || this.graph._nodes; + if (typeof nodes == "string") nodes = [nodes]; + for (var i in nodes) { + var node = nodes[i]; + if (node.is_selected) { + this.deselectNode(node); + continue; + } + + if (!node.is_selected && node.onSelected) { + node.onSelected(); + } + node.is_selected = true; + this.selected_nodes[node.id] = node; + + if (node.inputs) { + for (var j = 0; j < node.inputs.length; ++j) { + this.highlighted_links[node.inputs[j].link] = true; + } + } + if (node.outputs) { + for (var j = 0; j < node.outputs.length; ++j) { + var out = node.outputs[j]; + if (out.links) { + for (var k = 0; k < out.links.length; ++k) { + this.highlighted_links[out.links[k]] = true; + } + } + } + } + } + + this.setDirty(true); +}; + +LGraphCanvas.prototype.deselectNodeNexus = function(node) { + + if (!node.is_selected) { + return; + } + + if (node.onDeselected) { + node.onDeselected(); + } + + node.is_selected = false; + + //remove highlighted + if (node.inputs) { + for (var i = 0; i < node.inputs.length; ++i) { + delete this.highlighted_links[node.inputs[i].link]; + } + } + if (node.outputs) { + for (var i = 0; i < node.outputs.length; ++i) { + var out = node.outputs[i]; + if (out.links) { + for (var j = 0; j < out.links.length; ++j) { + delete this.highlighted_links[out.links[j]]; + } + } + } + } +}; + +LGraphGroup.prototype.move = function (deltax, deltay, ignore_nodes) { + this._pos[0] += deltax; + this._pos[1] += deltay; + if (ignore_nodes) { + return; + } + for (var i = 0; i < this._nodes.length; ++i) { + var node = this._nodes[i]; + node.pos[0] += deltax; + node.pos[1] += deltay; + if (this.onGroupNodeMoved) + this.onGroupNodeMoved(node) + } + if (this.onGroupMoved) + this.onGroupMoved(this) +}; + +LGraphGroup.prototype.configure = function (o) { + this.title = o.title; + this.owner = o.owner; + this.uuid = o.uuid; + this._bounding.set(o.bounding); + this.color = o.color; + if (o.font_size) { + this.font_size = o.font_size; + } +}; + +LGraphGroup.prototype.serialize = function () { + var b = this._bounding; + return { + title: this.title, + bounding: [ + Math.round(b[0]), + Math.round(b[1]), + Math.round(b[2]), + Math.round(b[3]) + ], + uuid: this.uuid, + color: this.color, + font_size: this.font_size + }; +}; + +LGraph.prototype.addNode = function (nodeData) { + let node = LiteGraph.createNode(nodeData.type, nodeData.title); + if (!node) { + console.warn("Node type not found: " + nodeData.type); + return false; + } + node.id = nodeData.id; + node.configureNexus(nodeData); + this.addNexus(node); + return true; +}; + +LGraph.prototype.removeNode = function (data) { + let node = this.getNodeById(data.id); + if (!node) { + console.warn("Node ID not found: " + data); + return false; + } + this.removeNexus(node); + return true; +}; + +LGraph.prototype.addLink = function (linkData) { + let link = new LiteGraph.LLink(); + link.configure(linkData); + this.links[link.id] = link; + return; +}; + +LGraph.prototype.addGroup = function (data) { + var group = new LiteGraph.LGraphGroup(); + group.configure(data); + this._groups.push(group); + this.setDirtyCanvas(true); + this.change(); + group.graph = this; + this._version++; + if (this.onGroupAdded) + this.onGroupAdded(group) + return; +}; + +LGraph.prototype.removeGroup = function (group) { + let existingGroupIndex = this.getGroupIndexByUUID(group.uuid); + if (existingGroupIndex != -1) { + this._groups.splice(existingGroupIndex, 1); + } + group.graph = null; + this._version++; + this.setDirtyCanvas(true, true); + this.change(); + return; +}; + +LGraph.prototype.updateNode = function (nodeData, type = "all") { + let existingNode = this.getNodeById(nodeData.id); + if (existingNode) { + if (type == "all") { + existingNode.configureNexus(nodeData); + } else if (type == "widgets") { + if (existingNode.widgets) { + for (var i = 0; i < existingNode.widgets.length; ++i) { + var w = existingNode.widgets[i]; + if (!w) + continue; + if (w.options && w.options.property && (existingNode.properties[w.options.property] != undefined)) + w.value = JSON.parse(JSON.stringify(existingNode.properties[w.options.property])); + } + if (nodeData.widgets_values) { + for (var i = 0; i < nodeData.widgets_values.length; ++i) { + if (existingNode.widgets[i]) { + existingNode.widgets[i].value = nodeData.widgets_values[i]; + } + } + } + } + } + } else { + this.addNode(nodeData); + } +}; + +LGraph.prototype.updateLink = function (linkData) { + + if (linkData) { + + let id + + if (linkData instanceof Object) { + id = linkData.id + } else { + id = linkData[0] + } + + let existingLink = this.links[id]; + if (existingLink) { + existingLink.configure(linkData); + } else { + this.addLink(linkData); + } + + } + +}; + +LGraph.prototype.updateLinkManual = function (change, linkData, node, graph) { + if (change == "add") { + this.updateLink(linkData) + } else if (change == "remove") { + let existingLink = this.links[linkData[0]]; + if (existingLink) { + this.removeLink(linkData[0]) + delete this.links[linkData[0]] + } + } + if (node) { + this.updateNode(node) + } + graph.updateExecutionOrder(); + graph._version++; + graph.setDirtyCanvas(true, true); +}; + +LGraph.prototype.updateGroup = function (groupData) { + let existingGroup = this.getGroupByUUID(groupData.uuid); + if (existingGroup) { + existingGroup.configure(groupData); + } else { + this.addGroup(groupData); + } +}; + +LGraph.prototype.updateNodes = function (newNodes) { + newNodes.forEach(nodeData => this.updateNode(nodeData)); +}; + +LGraph.prototype.updateLinks = function (newLinks) { + newLinks.forEach(linkData => this.updateLink(linkData)); +}; + +LGraph.prototype.updateGroups = function (newGroups) { + newGroups.forEach(groupData => this.updateGroup(groupData)); +}; \ No newline at end of file diff --git a/web/lib/nexus.mouse.js b/web/lib/nexus.mouse.js new file mode 100644 index 0000000..148747d --- /dev/null +++ b/web/lib/nexus.mouse.js @@ -0,0 +1,190 @@ +export class NexusMouseManager { + + hexColors = ["#1abc9c","#2ecc71","#3498db","#9b59b6","#f1c40f","#e67e22","#e74c3c","#ecf0f1"] + + createMouseSvg(color) { + return ` + + ` + } + + svgStringToImage(svgString) { + const blob = new Blob([svgString], { type: 'image/svg+xml' }); + const url = URL.createObjectURL(blob); + const image = new Image(); + image.src = url; + return image; + } + + hex2rgb(hex) { + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + return { r, g, b }; + } + + getRandomHexColor() { + return this.hexColors[Math.floor(Math.random() * this.hexColors.length)]; + } + + constructor(nexusSocket, app, fps) { + + this.color = this.getRandomHexColor() + + this.app = app + this.fps = fps + + this.canSendMouse = false + + this.nexusSocket = nexusSocket + this.nexusSocket.onConnected.push(this.handleConnect.bind(this)) + this.nexusSocket.onMessageReceive.push(this.handleMessageReceive.bind(this)) + + document.addEventListener("visibilitychange", this.handleVisiblityChange.bind(this)) + document.addEventListener('mousemove', this.handleSendMouseControl.bind(this)); + this.app.graph.list_of_graphcanvas[0].onDrawForeground = this.handlePointerDraw.bind(this) + + setInterval(this.handleSendMouse.bind(this), 1000 / this.fps) + + } + + throttle(func, limit) { + let lastFunc; + let lastRan; + return function (...args) { + const context = this; + if (!lastRan) { + func.apply(context, args); + lastRan = Date.now(); + } else { + clearTimeout(lastFunc); + lastFunc = setTimeout(function () { + if ((Date.now() - lastRan) >= limit) { + func.apply(context, args); + lastRan = Date.now(); + } + }, limit - (Date.now() - lastRan)); + } + }; + } + + handleCanvasDraw() { + if (this.app.graph && this.app.graph.list_of_graphcanvas && this.app.graph.list_of_graphcanvas.length > 0) { + this.app.graph.list_of_graphcanvas[0].draw(true, true); + } + } + + handleConnect() { + this.drawTimer = setInterval(this.handleCanvasDraw.bind(this), 1000 / this.fps) + } + + handleMessageReceive(message) { + + let name = message.name; + let from = message.from; + let data = message.data + + if (name == "mouse") { + let mouse = this.nexusSocket.userManager.get(from, "mouse"); + let pointer = this.nexusSocket.userManager.get(from, "pointer"); + if (!pointer) { + this.nexusSocket.userManager.set(from, "pointer", this.svgStringToImage(this.createMouseSvg(data.mouse[3]))); + } else if (pointer && mouse && mouse[3] != data.mouse[3]) { + this.nexusSocket.userManager.set(from, "pointer", this.svgStringToImage(this.createMouseSvg(data.mouse[3]))); + } + this.nexusSocket.userManager.set(from, "mouse", data.mouse) + } else if (name == "hide_mouse") { + this.nexusSocket.userManager.set(from, "mouse", null) + } else if (name== "mouse_view_toggle") { + this.nexusSocket.userManager.set(from, "mouse_view", data.on) + if(data.on == 0){ + this.nexusSocket.sendMessage("hide_mouse", { receiver: from }, false) + } + } + + } + + handleVisiblityChange() { + if (document.visibilityState === "hidden") { + clearInterval(this.drawTimer) + this.nexusSocket.sendMessage("hide_mouse", {}, false) + this.nexusSocket.sendMessage("afk", {}, false) + } else { + this.handleConnect() + this.nexusSocket.sendMessage("online", {}, false) + } + } + + handleSendMouse = function () { + if (this.app.graph && this.app.graph.list_of_graphcanvas && this.app.graph.list_of_graphcanvas.length > 0) { + this.mouse = this.app.graph.list_of_graphcanvas[0].canvas_mouse + this.scale = this.app.graph.list_of_graphcanvas[0].ds.scale + } + this.mouse[3] = this.color + this.mouse[4] = this.scale / 10 + + if (this.canSendMouse) { + let users = Object.keys(this.nexusSocket.userManager.users) + for (let index = 0; index < users.length; index++) { + let user_id = users[index] + let view = this.nexusSocket.userManager.get(user_id, "mouse_view"); + if(view != null) { + if(view == 1) { + this.nexusSocket.sendMessage('mouse', { mouse: this.mouse, receiver: user_id }, false) + } + } + else { + this.nexusSocket.sendMessage('mouse', { mouse: this.mouse, receiver: user_id }, false) + } + } + this.canSendMouse = false + } + + } + + handleSendMouseControl() { + this.canSendMouse = true + } + + handlePointerDraw = function (ctx) { + Object.keys(this.nexusSocket.userManager.users).forEach(id => { + if (id != this.nexusSocket.uuid) { + + let mouse = this.nexusSocket.userManager.get(id, "mouse"); + let name = this.nexusSocket.userManager.get(id, "name") || "User"; + let pointerImage = this.nexusSocket.userManager.get(id, "pointer"); + let hidden = this.nexusSocket.userManager.get(id, "mouse_hidden"); + + if(hidden == 0) { + mouse = null + } + + if (mouse && mouse.length > 0) { + + let mouseX = mouse[0] - 4; + let mouseY = mouse[1] - 4; + let scale = 1 - mouse[4]; + + ctx.drawImage(pointerImage, mouseX, mouseY, 18, 18); + ctx.font = '14px Arial'; + ctx.fillStyle = mouse[3]; + ctx.fillText(name, mouseX + 22, mouseY + 16); + + let color = this.hex2rgb(mouse[3]) + + ctx.beginPath(); + ctx.arc(mouseX, mouseY, scale * 600, 0, 2 * Math.PI, false); + ctx.strokeStyle = `rgba(${color.r},${color.g},${color.b},0.1)`; + ctx.lineWidth = 2; + ctx.stroke(); + + } + } + + }); + } +} \ No newline at end of file diff --git a/web/lib/nexus.panel.js b/web/lib/nexus.panel.js new file mode 100644 index 0000000..41f433d --- /dev/null +++ b/web/lib/nexus.panel.js @@ -0,0 +1,258 @@ +import { DynamicElement } from "./nexus.elements.js" +import { StyleInjector } from "./nexus.styles.js"; + +export class UserPanel { + + constructor() { + + this.active = 0 + + this.panel = new DynamicElement({ + id: 'nexus-player-panel', + tag: 'div', + visible: false, + styles: { + "width": "100%", + "height": "100vh", + "display": "flex", + "align-items": "center", + "justify-content": "center", + "box-sizing": "border-box", + "position": "absolute", + "left": "0", + "top": "0", + "z-index": "10000" + }, + childrens: [ + new DynamicElement({ + id: 'nexus-player-panel-table', + tag: 'div', + visible: true, + styles: { + "width": "832px", + "display": "flex", + "flex-direction": "column", + "border-radius": "6px", + "box-sizing": "border-box" + }, + childrens: [ + new DynamicElement({ + id: 'nexus-player-panel-table-body', + tag: 'div', + visible: true, + styles: { + "width": "100%", + "display": "flex", + "flex-direction": "column", + "align-items": "center", + "justify-content": "flex-start", + "color": "#a3a3a3", + "font-family": "Calibri", + "font-weight": "600", + "gap": "12px", + "padding-bottom": "12px", + "box-sizing": "border-box", + "background": "rgba(54,54,54,0.8)", + "border-radius": "6px", + "box-shadow": "4px 8px 8px rgba(0,0,0,0.5)", + "max-height": "540px", + "overflowY": "auto" + } + }), + new StyleInjector({ + styles: { + "#nexus-player-panel .table-row": { + "width": "100%", + "display": "grid", + "align-items": "center", + "grid-template-columns": "20% 60% 20%", + "justify-content": "space-between", + }, + "#nexus-player-panel .table-row:first-child": { + "width": "100%", + "color": "#a3a3a3", + "padding": "16px 0", + "background": "rgb(0,0,0,0.6)", + "border-top-left-radius": "6px", + "border-top-right-radius": "6px", + "position": "sticky", + "top": "0", + }, + "#nexus-player-panel .table-row .table-row-child": { + "display": "flex", + "justify-content": "flex-start", + "gap": "12px", + "padding-left": "12px", + "width": "100%" + }, + "#nexus-player-panel .table-row-child button": { + "border": "none", + "padding": "6px 12px", + "border-radius": "3px", + "cursor": "pointer", + "background": "rgba(94,94,94)", + "color": "#a1a1a1", + }, + "#nexus-player-panel .table-row-child button:active": { + "opacity": "0.8" + }, + "#nexus-player-panel .table-row-child button[active='1'][label='spectate']": { + "background": "#218c74", + "color": "#fafafa" + }, + "#nexus-player-panel .table-row-child button[active='1'][label='editor']": { + "background": "#474787", + "color": "#fafafa" + }, + "#nexus-player-panel .table-row-child button[active='1'][label='mouse']": { + "background": "#cd6133", + "color": "#fafafa" + }, + "#nexus-player-panel .table-row-child button[active='1'][label='queue']": { + "background": "#6F1E51", + "color": "#fafafa" + } + } + }) + ] + }) + ], + }) + this.onCommandReceived = null + this.buttons = [] + this.columns = [] + } + + addColumnsToPanel(columns) { + + this.columns = columns + + } + + addRowsToPanel(rows, callbacks, values, data) { + + let tbody = this.panel.childrens[0].childrens[0]; + tbody.removeChildrens(); + this.buttons = [] + + const tr = document.createElement('div'); + tr.className = "table-row"; + + this.columns.forEach(heading => { + const th = document.createElement('span'); + th.className = "table-row-child" + th.textContent = heading; + tr.appendChild(th) + }); + + tbody.addChild(tr) + + rows.forEach((row, i) => { + const tr = document.createElement('div'); + tr.className = "table-row"; + + row.forEach((cell, j) => { + + let td = document.createElement('span'); + td.className = "table-row-child" + + let hascomma = cell.indexOf(",") > -1 + let hascolon = cell.indexOf(":") > -1 + + let content = [] + + if (hascomma) { + content = cell.split(',') + content.forEach((elm, k) => { + let elmcontent = elm.split(':') + this.addChildToParent(elmcontent, td, callbacks, values, data, i, j, k) + }) + } + else if (hascolon) { + content = cell.split(':') + this.addChildToParent(content, td, callbacks, values, data, i, j, -1) + } else { + td.textContent = cell + } + tr.appendChild(td); + + }); + tbody.addChild(tr) + }); + } + + addChildToParent(content, td, callbacks, values, data, i, j, k) { + //console.log(content, td, callbacks, values, data, i, j, k) + switch (content[0]) { + case 'button': + let btn = document.createElement('button'); + if (content[1] == 'none') { + btn.className = "none" + } + if (!this.buttons.includes(btn)) { + this.buttons.push(btn) + } + btn.textContent = content[1] + if (callbacks[i] && callbacks[i][j] && values[i][j] && data[i][j]) { + let inner_values = values[i][j].split(',') + let inner_data = data[i][j].split(',') + if (k != -1) { + btn.setAttribute('data', inner_data[k]); + btn.setAttribute('active', inner_values[k]); + } else { + btn.setAttribute('data', data[i][j]); + btn.setAttribute('active', values[i][j]); + } + btn.setAttribute('kind', content[0]); + btn.setAttribute('label', content[1]); + if (content[1] != 'none') { + btn.onclick = callbacks[i][j].bind(this); + } + } + td.appendChild(btn) + break; + } + + } + + disableButtons(label) { + this.buttons.forEach(btn => { + let btn_kind = btn.getAttribute('kind') + let btn_data = btn.getAttribute('data') + let btn_active = parseInt(btn.getAttribute('active')) + let btn_label = btn.getAttribute('label') + if (btn_label == label) { + if (btn_active == 1) { + btn.setAttribute('active', 0) + //console.log(btn_kind, btn_label, btn_data, 0) + if (this.onCommandReceived) { + this.onCommandReceived(btn_kind, btn_label, btn_data, 0) + } + } + } + }) + } + + onclick(e) { + + let kind = e.target.getAttribute('kind') + let data = e.target.getAttribute('data') + let label = e.target.getAttribute('label') + let active = parseInt(e.target.getAttribute('active')) + if (kind == "button") { + if (label == "spectate") { + this.disableButtons(label) + } + if (active == 0) { + e.target.setAttribute('active', 1) + } else if (active == 1) { + e.target.setAttribute('active', 0) + } + } + active = parseInt(e.target.getAttribute('active')) + //console.log(kind, label, data, active) + if (this.onCommandReceived) { + this.onCommandReceived(kind, label, data, active) + } + } +} \ No newline at end of file diff --git a/web/lib/nexus.sockets.js b/web/lib/nexus.sockets.js new file mode 100644 index 0000000..a2271a9 --- /dev/null +++ b/web/lib/nexus.sockets.js @@ -0,0 +1,135 @@ +import { UserManager } from "./nexus.user.manager.js"; + +export class NexusSocket extends EventTarget { + + getName() { + let name = localStorage.getItem('nexus-socket-name'); + if (!name) { + localStorage.setItem('nexus-socket-name', "User"); + name = "User" + } + this.userManager.set(this.uuid, "name", name) + return name + } + + getUUID() { + let uuid = localStorage.getItem('nexus-socket-uuid'); + if (!uuid) { + uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => + (Math.random() * 16 | 0).toString(16) + ); + localStorage.setItem('nexus-socket-uuid', uuid); + } + return uuid; + } + + constructor() { + + super(); + this.ws = null; + this.userManager = new UserManager(); + this.uuid = this.getUUID(); + this.admin = false + this.wsUrl = `ws${window.location.protocol === "https:" ? "s" : ""}://${location.host}/nexus?id=${this.uuid}`; + + this.onConnected = [this.onConnectedHandler.bind(this)]; + this.onDisconnected = []; + this.onMessageReceive = [this.onMessageReceiveHandler.bind(this)]; + this.onMessageSend = []; + this.onError = []; + this.onClose = []; + + this.getName() + + } + + onConnectedHandler() { + this.userManager.create(this.uuid); + this.sendMessage('join', { name: this.getName() }, false); + this.sendMessage("online", {}, false) + } + + onMessageReceiveHandler(message) { + let name = message.name; + let from = message.from; + switch (name) { + case "join": + this.userManager.create(from); + this.sendMessage('hi', {}, false); + break; + case "hi": + this.userManager.create(from); + break; + } + } + + connect() { + try { + this.ws = new WebSocket(this.wsUrl); + this.ws.onopen = () => { + if (this.onConnected) { + this.onConnected.forEach(f => { f(); }); + } + }; + this.ws.onmessage = (msg) => { + try { + const message = JSON.parse(msg.data); + if (this.onMessageReceive) { + this.onMessageReceive.forEach(f => { f(message); }); + } + } catch (e) { + console.error('Error handling message:', e); + } + }; + this.ws.onerror = (error) => { + if (this.onError) { + this.onError.forEach(f => { f(error); }); + } + console.error('WebSocket error:', error); + }; + this.ws.onclose = () => { + if (this.onDisconnected) { + this.onDisconnected.forEach(f => { f(); }); + } + setTimeout(() => { + this.connect(); + }, 5000); + }; + } catch (e) { + console.error('Error connecting to WebSocket:', e); + } + } + + closeWebSocket() { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.close(); + } else { + console.log('WebSocket is not open.'); + } + } + + sendMessage(event_name, event_data, all = true) { + try { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + let data = {} + if(event_data.receiver) { + data.receiver = event_data.receiver + delete event_data.receiver + } else if (event_data.exclude) { + data.exclude = event_data.exclude + delete event_data.exclude + } + data.from = this.uuid + data.name = event_name + data.data = event_data + data.all = all + this.ws.send(JSON.stringify(data)); + if (this.onMessageSend) { + this.onMessageSend.forEach(f => { f(message); }); + } + } + } catch (e) { + console.error(`Error sending ${event_name} message:`, e); + } + } +} diff --git a/web/lib/nexus.styles.js b/web/lib/nexus.styles.js new file mode 100644 index 0000000..a46bc3b --- /dev/null +++ b/web/lib/nexus.styles.js @@ -0,0 +1,37 @@ +export class StyleInjector { + + constructor({ styles }) { + this.el = null + this.styles = styles; + this.create() + } + + create() { + this.el = document.createElement('style'); + let cssString = ""; + for (const selector in this.styles) { + cssString += `${selector} { `; + for (const property in this.styles[selector]) { + cssString += `${property}: ${this.styles[selector][property]}; `; + } + cssString += `} `; + } + this.el.textContent = cssString; + } + + softUpdate(selector, property, value) { + let cssString = ""; + for (const key in this.styles) { + cssString += `${key} { `; + for (const property in this.styles[key]) { + cssString += `${property}: ${this.styles[key][property]}; `; + } + if(key == selector) { + cssString += `${property}: ${value}; `; + } + cssString += `} `; + } + this.el.textContent = cssString; + } + +} \ No newline at end of file diff --git a/web/lib/nexus.user.manager.js b/web/lib/nexus.user.manager.js new file mode 100644 index 0000000..263e8cb --- /dev/null +++ b/web/lib/nexus.user.manager.js @@ -0,0 +1,27 @@ +export class UserManager { + constructor() { + this.users = {} + } + + create(id) { + if (!this.users[id]) { + this.users[id] = {} + } + } + + set(id, key, data) { + this.create(id) + this.users[id][key] = data + } + + get(id, key) { + this.create(id) + return this.users[id][key] + } + + remove(id) { + if(this.users[id]) + delete this.users[id]; + return true + } +} diff --git a/web/lib/nexus.workflow.js b/web/lib/nexus.workflow.js new file mode 100644 index 0000000..9c0827f --- /dev/null +++ b/web/lib/nexus.workflow.js @@ -0,0 +1,306 @@ +import "./nexus.litegraph.js" +import { postWorkflow, postBackupWorkflow, getLatestWorkflow } from "./nexus.api.js"; + +export class NexusWorkflowManager { + + constructor(app, api, nexusSocket) { + + this.app = app; + this.api = api; + + this.nexusSocket = nexusSocket; + this.nexusSocket.onMessageReceive.push(this.handleMessageReceive.bind(this)) + + this.init() + + this.spectators = [] + this.saveTime = 0 + setInterval(this.handleSpectators.bind(this), 1000 / 24) // 24 fps + + } + + async init() { + let workflow = (await this.app.graphToPrompt()).workflow; + await postBackupWorkflow(this.nexusSocket.uuid, workflow) + this.app.graph.clear() + } + + removeSaveManager() { + if (this.saveTimer) { + clearInterval(this.saveTimer) + } + // document.removeEventListener("visibilitychange", this.handleSaveWorkflowManager.bind(this)) + } + + async saveManager(time) { + if (this.nexusSocket.admin) { + this.nexusSocket.sendMessage('workflow:timer', { time: time - 2 }, false) + } + this.removeSaveManager() + this.saveTime = time + this.saveTimer = setInterval(this.sendWorkFlow.bind(this), time * 1000) + console.log("Auto Backup Timer Setup:",time) + // document.addEventListener("visibilitychange", this.handleSaveWorkflowManager.bind(this)) + } + + async sendWorkFlow() { + let workflow = (await this.app.graphToPrompt()).workflow; + await postWorkflow(this.nexusSocket.uuid, workflow) + } + + async loadLatestWorkflow() { + let workflow = await getLatestWorkflow() + let links = workflow.links || []; + let nodes = workflow.nodes || []; + let groups = workflow.groups || []; + this.app.graph.updateNodes(nodes); + this.app.graph.updateLinks(links); + this.app.graph.updateGroups(groups); + this.app.graph.updateExecutionOrder(); + this.app.graph.setDirtyCanvas(true, true); + } + + async handleSaveWorkflowManager() { + this.sendWorkFlow() + // if (document.visibilityState === "hidden") { + // if (this.saveTimer) { + // clearInterval(this.saveTimer) + // } + // } + // else { + // this.saveTimer = setInterval(this.sendWorkFlow.bind(this), this.saveTime * 1000) + // } + } + + async handleSpectators() { + if (this.spectators.length > 0) { + let workflow = (await this.app.graphToPrompt()).workflow; + let wfe = workflow.extra + for (let index = 0; index < this.spectators.length; index++) { + const id = this.spectators[index]; + this.nexusSocket.sendMessage('workflow:spectate', { workflow: wfe, receiver: id }, false) + } + } + } + + async handleMessageReceive(message) { + + let from = message.from; + let data = message.data + let name = message.name; + + switch (name) { + case "spectate": + if (data.on == 0) { + if (this.spectators.includes(from)) { + this.spectators.splice(this.spectators.indexOf(from), 1) + } + } else if (data.on == 1) { + if (!this.spectators.includes(from)) { + this.spectators.push(from) + } + } + case "join": + // this.sendWorkFlow() + this.saveManager(this.saveTime || 60) + break; + case "workflow:spectate": + let wfe = data.workflow + if (wfe?.ds) { + this.app.canvas.ds.offset = wfe.ds.offset; + this.app.canvas.ds.scale = wfe.ds.scale; + } + break; + case "workflow:timer": + let time = data.time + this.saveManager(time) + break; + case "workflow:update": + this.handleWorkflowUpdate(from, data) + break; + case "workflow": + let workflow = data.workflow + if (workflow.clear) { + await this.app.graph.clear() + } + let links = workflow.links || []; + let nodes = workflow.nodes || []; + let groups = workflow.groups || []; + this.app.graph.updateNodes(nodes); + this.app.graph.updateLinks(links); + this.app.graph.updateGroups(groups); + this.app.graph.updateExecutionOrder(); + this.app.graph.setDirtyCanvas(true, true); + } + + } + + handleWorkflowEvents(oN) { + if (oN) { + this.app.graph.onNodeAdded = this.onNodeAdded.bind(this); + this.app.graph.onNodeAddedNexus = this.onNodeAddedNexus.bind(this); + this.app.graph.onNodeRemoved = this.onNodeRemoved.bind(this); + this.app.canvas.onSelectionChange = this.onSelectionChange.bind(this); + this.app.canvas.onNodeDeselected = this.onNodeDeselected.bind(this); + this.app.canvas.onNodeMoved = this.onNodeMoved.bind(this); + this.app.graph.onGroupAdded = this.onGroupAdded.bind(this); + this.app.graph.onGroupRemoved = this.onGroupRemoved.bind(this); + this.app.graph._groups.forEach(group => { + group.onGroupMoved = this.onGroupMoved.bind(this, group); + group.onGroupNodeMoved = this.onNodeMoved.bind(this); + }); + this.app.graph._nodes.forEach(node => { + node.onWidgetChanged = this.onWidgetChanged.bind(this, node); + node.onConnectionsChange = this.onNodeConnectionChange.bind(this, node); + }); + } + else { + this.app.graph.onNodeAddedNexus = null; + this.app.graph.onNodeAdded = null; + this.app.graph.onNodeRemoved = null; + this.app.canvas.onSelectionChange = null; + this.app.canvas.onNodeDeselected = null; + this.app.canvas.onNodeMoved = null; + this.app.graph.onGroupAdded = null; + this.app.graph.onGroupRemoved = null; + this.app.graph._groups.forEach(group => { + group.onGroupMoved = null; + group.onGroupNodeMoved = null; + }); + this.app.graph._nodes.forEach(node => { + node.onWidgetChanged = null; + node.onConnectionsChange = null; + }); + } + } + + handleWorkflowUpdate(from, detail) { + + let graph = this.app.graph; + let canvas = this.app.canvas; + let update = detail.update; + let data = detail.data; + + let existingNode; + + switch (update) { + case "node-added": + graph.updateNode(data); + break; + case "node-removed": + graph.removeNode(data); + break; + case "node-moved": + graph.updateNode(data); + break; + case "node-widget-changed": + graph.updateNode(data); + break; + case "node-selection": + let nodes = [] + data.forEach(node => { + existingNode = graph.getNodeById(node.id); + if (existingNode) { + nodes.push(existingNode) + } + }) + canvas.selectNodesNexus(nodes, true) + break; + case "node-deselection": + existingNode = graph.getNodeById(data.id); + if (existingNode) + canvas.deselectNodeNexus(existingNode) + break; + case "group-added": + graph.updateGroup(data); + break; + case "group-moved": + graph.updateGroup(data); + break; + case "group-removed": + graph.removeGroup(data); + break; + case "node-connection": + graph.updateLinkManual(detail.change, detail.link, detail.node, graph); + break; + default: + //console.log(update); + break; + } + + } + + onNodeAddedNexus(node) { + node.onWidgetChanged = this.onWidgetChanged.bind(this, node); + node.onConnectionsChange = this.onNodeConnectionChange.bind(this, node); + } + + onNodeAdded(node) { + let data = node.serialize(); + node.onWidgetChanged = this.onWidgetChanged.bind(this, node); + node.onConnectionsChange = this.onNodeConnectionChange.bind(this, node); + this.nexusSocket.sendMessage('workflow:update', { update: "node-added", data }, false); + } + + onWidgetChanged(node) { + let data = node.serialize(); + this.nexusSocket.sendMessage('workflow:update', { update: "node-widget-changed", data }, false); + } + + onNodeRemoved(node) { + let data = node.serialize(); + this.nexusSocket.sendMessage('workflow:update', { update: "node-removed", data }, false); + } + + async onNodeConnectionChange(node, a, b, c, d, e) { + let change = c ? "add" : "remove"; + let link = d; + this.graphData = (await this.app.graphToPrompt()).workflow; + let data = this.graphData.links; + //console.log(data) + node = this.app.graph.getNodeById(node.id); + node = node ? node.serialize() : null; + this.nexusSocket.sendMessage('workflow:update', { node, update: "node-connection", link, change, data }, false); + } + + onSelectionChange(nodes) { + let data = [] + Object.keys(nodes).forEach(node_id => { + data.push(nodes[node_id].serialize()) + }) + this.nexusSocket.sendMessage('workflow:update', { update: "node-selection", data }, false); + } + + onNodeDeselected(node) { + let data = node.serialize() + this.nexusSocket.sendMessage('workflow:update', { update: "node-deselection", data }, false); + } + + onNodeMoved(node) { + let data = node.serialize(); + this.nexusSocket.sendMessage('workflow:update', { update: "node-moved", data }, false); + } + + onGroupAdded(group) { + let data = group.serialize(); + let uuid = localStorage.getItem('nexus-socket-uuid'); + if (uuid) { + data.owner = uuid + group.configure(data); + } + group.onGroupMoved = this.onGroupMoved.bind(this, group); + group.onGroupNodeMoved = this.onNodeMoved.bind(this); + this.nexusSocket.sendMessage('workflow:update', { update: "group-added", data }, false); + } + + onGroupRemoved(group) { + let data = group.serialize(); + this.nexusSocket.sendMessage('workflow:update', { update: "group-removed", data }, false); + } + + onGroupMoved(group) { + let data = group.serialize(); + this.nexusSocket.sendMessage('workflow:update', { update: "group-moved", data }, false); + } + +} diff --git a/web/lib/nexus.workflow.panel.js b/web/lib/nexus.workflow.panel.js new file mode 100644 index 0000000..b8a4f71 --- /dev/null +++ b/web/lib/nexus.workflow.panel.js @@ -0,0 +1,236 @@ +import { DynamicElement } from "./nexus.elements.js" +import { StyleInjector } from "./nexus.styles.js"; + +export class WorkflowPanel { + + constructor() { + + this.active = 0 + + this.panel = new DynamicElement({ + id: 'nexus-workflow-panel', + tag: 'div', + visible: false, + styles: { + "width": "100%", + "height": "100vh", + "display": "flex", + "align-items": "center", + "justify-content": "center", + "box-sizing": "border-box", + "position": "absolute", + "left": "0", + "top": "0", + "z-index": "10000" + }, + childrens: [ + new DynamicElement({ + id: 'nexus-workflow-panel-table', + tag: 'div', + visible: true, + styles: { + "width": "512px", + "display": "flex", + "flex-direction": "column", + "border-radius": "6px", + "box-sizing": "border-box" + }, + childrens: [ + new DynamicElement({ + id: 'nexus-workflow-panel-table-body', + tag: 'div', + visible: true, + styles: { + "width": "100%", + "display": "flex", + "flex-direction": "column", + "align-items": "center", + "justify-content": "flex-start", + "color": "#a3a3a3", + "font-family": "Calibri", + "font-weight": "600", + "gap": "12px", + "padding-bottom": "12px", + "box-sizing": "border-box", + "background": "rgba(54,54,54,0.8)", + "border-radius": "6px", + "box-shadow": "4px 8px 8px rgba(0,0,0,0.5)", + "max-height": "540px", + "overflowY": "auto" + } + }), + new StyleInjector({ + styles: { + "#nexus-workflow-panel .table-row": { + "width": "100%", + "display": "flex", + "align-items": "center", + "justify-content": "center", + }, + "#nexus-workflow-panel .table-row:first-child": { + "width": "100%", + "color": "#a3a3a3", + "padding": "16px 0", + "background": "rgb(0,0,0,0.6)", + "border-top-left-radius": "6px", + "border-top-right-radius": "6px", + "position": "sticky", + "top": "0", + }, + "#nexus-workflow-panel .table-row .table-row-child": { + "display": "flex", + "justify-content": "flex-start", + "gap": "12px", + "padding-left": "12px", + "width": "100%" + }, + "#nexus-workflow-panel .table-row-child button": { + "border": "none", + "padding": "6px 12px", + "border-radius": "3px", + "cursor": "pointer", + "background": "rgba(94,94,94)", + "color": "#a1a1a1", + "width": "100%" + }, + "#nexus-workflow-panel .table-row-child:first-child": { + "width":"50%" + }, + "#nexus-workflow-panel .table-row-child:last-child": { + "padding-right":"12px" + }, + "#nexus-workflow-panel .table-row-child button:active": { + "opacity": "0.8" + }, + "#nexus-workflow-panel .table-row-child button[label='load']:active": { + "background": "#218c74", + "color": "#fafafa" + }, + "#nexus-workflow-panel .table-row-child button[label='loadforall']:active": { + "background": "#474787", + "color": "#fafafa" + } + } + }) + ] + }) + ], + }) + this.onCommandReceived = null + this.buttons = [] + this.columns = [] + } + + addColumnsToPanel(columns) { + + this.columns = columns + + } + + addRowsToPanel(rows, callbacks, values, data) { + + let tbody = this.panel.childrens[0].childrens[0]; + tbody.removeChildrens(); + this.buttons = [] + + const tr = document.createElement('div'); + tr.className = "table-row"; + + this.columns.forEach(heading => { + const th = document.createElement('span'); + th.className = "table-row-child" + th.textContent = heading; + tr.appendChild(th) + }); + + tbody.addChild(tr) + + rows.forEach((row, i) => { + const tr = document.createElement('div'); + tr.className = "table-row"; + + row.forEach((cell, j) => { + + let td = document.createElement('span'); + td.className = "table-row-child" + + let hascomma = cell.indexOf(",") > -1 + let hascolon = cell.indexOf(":") > -1 + + let content = [] + + if (hascomma) { + content = cell.split(',') + content.forEach((elm, k) => { + let elmcontent = elm.split(':') + this.addChildToParent(elmcontent, td, callbacks, values, data, i, j, k) + }) + } + else if (hascolon) { + content = cell.split(':') + this.addChildToParent(content, td, callbacks, values, data, i, j, -1) + } else { + cell = cell.split("|") + cell = cell.join(":") + td.textContent = cell + } + tr.appendChild(td); + + }); + tbody.addChild(tr) + }); + } + + addChildToParent(content, td, callbacks, values, data, i, j, k) { + switch (content[0]) { + case 'button': + let btn = document.createElement('button'); + if (content[1] == 'none') { + btn.className = "none" + } + if (!this.buttons.includes(btn)) { + this.buttons.push(btn) + } + btn.textContent = content[1] + if (callbacks[i] && callbacks[i][j] && values[i][j] && data[i][j]) { + let inner_values = values[i][j].split(',') + let inner_data = data[i][j].split(',') + if (k != -1) { + btn.setAttribute('data', inner_data[k]); + btn.setAttribute('active', inner_values[k]); + } else { + btn.setAttribute('data', data[i][j]); + btn.setAttribute('active', values[i][j]); + } + btn.setAttribute('kind', content[0]); + btn.setAttribute('label', content[1]); + if (content[1] != 'none') { + btn.onclick = callbacks[i][j].bind(this); + } + } + td.appendChild(btn) + break; + } + + } + + onclick(e) { + + let kind = e.target.getAttribute('kind') + let data = e.target.getAttribute('data') + let label = e.target.getAttribute('label') + let active = parseInt(e.target.getAttribute('active')) + if (kind == "button") { + if (active == 0) { + e.target.setAttribute('active', 1) + } else if (active == 1) { + e.target.setAttribute('active', 0) + } + } + active = parseInt(e.target.getAttribute('active')) + //console.log(kind, label, data, active) + if (this.onCommandReceived) { + this.onCommandReceived(kind, label, data, active) + } + } +} diff --git a/web/util.js b/web/util.js index 919bf83..15edb9e 100644 --- a/web/util.js +++ b/web/util.js @@ -1,11 +1,25 @@ -(()=>{var M=class{constructor(){this.users={}}create(e){this.users[e]||(this.users[e]={})}set(e,t,s){this.create(e),this.users[e][t]=s}get(e,t){return this.create(e),this.users[e][t]}remove(e){return this.users[e]&&delete this.users[e],!0}};var N=class extends EventTarget{getName(){let e=localStorage.getItem("nexus-socket-name");return e||(localStorage.setItem("nexus-socket-name","User"),e="User"),this.userManager.set(this.uuid,"name",e),e}getUUID(){let e=localStorage.getItem("nexus-socket-uuid");return e||(e="xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,t=>(Math.random()*16|0).toString(16)),localStorage.setItem("nexus-socket-uuid",e)),e}constructor(){super(),this.ws=null,this.userManager=new M,this.uuid=this.getUUID(),this.admin=!1,this.wsUrl=`ws${window.location.protocol==="https:"?"s":""}://${location.host}/nexus?id=${this.uuid}`,this.onConnected=[this.onConnectedHandler.bind(this)],this.onDisconnected=[],this.onMessageReceive=[this.onMessageReceiveHandler.bind(this)],this.onMessageSend=[],this.onError=[],this.onClose=[],this.getName()}onConnectedHandler(){this.userManager.create(this.uuid),this.sendMessage("join",{name:this.getName()},!1),this.sendMessage("online",{},!1)}onMessageReceiveHandler(e){let t=e.name,s=e.from;switch(t){case"join":this.userManager.create(s),this.sendMessage("hi",{},!1);break;case"hi":this.userManager.create(s);break}}connect(){try{this.ws=new WebSocket(this.wsUrl),this.ws.onopen=()=>{this.onConnected&&this.onConnected.forEach(e=>{e()})},this.ws.onmessage=e=>{try{let t=JSON.parse(e.data);this.onMessageReceive&&this.onMessageReceive.forEach(s=>{s(t)})}catch(t){console.error("Error handling message:",t)}},this.ws.onerror=e=>{this.onError&&this.onError.forEach(t=>{t(e)}),console.error("WebSocket error:",e)},this.ws.onclose=()=>{this.onDisconnected&&this.onDisconnected.forEach(e=>{e()}),setTimeout(()=>{this.connect()},5e3)}}catch(e){console.error("Error connecting to WebSocket:",e)}}closeWebSocket(){this.ws&&this.ws.readyState===WebSocket.OPEN?this.ws.close():console.log("WebSocket is not open.")}sendMessage(e,t,s=!0){try{if(this.ws&&this.ws.readyState===WebSocket.OPEN){let n={};t.receiver?(n.receiver=t.receiver,delete t.receiver):t.exclude&&(n.exclude=t.exclude,delete t.exclude),n.from=this.uuid,n.name=e,n.data=t,n.all=s,this.ws.send(JSON.stringify(n)),this.onMessageSend&&this.onMessageSend.forEach(o=>{o(message)})}}catch(n){console.error(`Error sending ${e} message:`,n)}}};var T=class{hexColors=["#1abc9c","#2ecc71","#3498db","#9b59b6","#f1c40f","#e67e22","#e74c3c","#ecf0f1"];createMouseSvg(e){return` - - `}svgStringToImage(e){let t=new Blob([e],{type:"image/svg+xml"}),s=URL.createObjectURL(t),n=new Image;return n.src=s,n}hex2rgb(e){let t=parseInt(e.slice(1,3),16),s=parseInt(e.slice(3,5),16),n=parseInt(e.slice(5,7),16);return{r:t,g:s,b:n}}getRandomHexColor(){return this.hexColors[Math.floor(Math.random()*this.hexColors.length)]}constructor(e,t,s){this.color=this.getRandomHexColor(),this.app=t,this.fps=s,this.canSendMouse=!1,this.nexusSocket=e,this.nexusSocket.onConnected.push(this.handleConnect.bind(this)),this.nexusSocket.onMessageReceive.push(this.handleMessageReceive.bind(this)),document.addEventListener("visibilitychange",this.handleVisiblityChange.bind(this)),document.addEventListener("mousemove",this.handleSendMouseControl.bind(this)),this.app.graph.list_of_graphcanvas[0].onDrawForeground=this.handlePointerDraw.bind(this),setInterval(this.handleSendMouse.bind(this),1e3/this.fps)}throttle(e,t){let s,n;return function(...o){let a=this;n?(clearTimeout(s),s=setTimeout(function(){Date.now()-n>=t&&(e.apply(a,o),n=Date.now())},t-(Date.now()-n))):(e.apply(a,o),n=Date.now())}}handleCanvasDraw(){this.app.graph&&this.app.graph.list_of_graphcanvas&&this.app.graph.list_of_graphcanvas.length>0&&this.app.graph.list_of_graphcanvas[0].draw(!0,!0)}handleConnect(){this.drawTimer=setInterval(this.handleCanvasDraw.bind(this),1e3/this.fps)}handleMessageReceive(e){let t=e.name,s=e.from,n=e.data;if(t=="mouse"){let o=this.nexusSocket.userManager.get(s,"mouse"),a=this.nexusSocket.userManager.get(s,"pointer");a?a&&o&&o[3]!=n.mouse[3]&&this.nexusSocket.userManager.set(s,"pointer",this.svgStringToImage(this.createMouseSvg(n.mouse[3]))):this.nexusSocket.userManager.set(s,"pointer",this.svgStringToImage(this.createMouseSvg(n.mouse[3]))),this.nexusSocket.userManager.set(s,"mouse",n.mouse)}else t=="hide_mouse"?this.nexusSocket.userManager.set(s,"mouse",null):t=="mouse_view_toggle"&&(this.nexusSocket.userManager.set(s,"mouse_view",n.on),n.on==0&&this.nexusSocket.sendMessage("hide_mouse",{receiver:s},!1))}handleVisiblityChange(){document.visibilityState==="hidden"?(clearInterval(this.drawTimer),this.nexusSocket.sendMessage("hide_mouse",{},!1),this.nexusSocket.sendMessage("afk",{},!1)):(this.handleConnect(),this.nexusSocket.sendMessage("online",{},!1))}handleSendMouse=function(){if(this.app.graph&&this.app.graph.list_of_graphcanvas&&this.app.graph.list_of_graphcanvas.length>0&&(this.mouse=this.app.graph.list_of_graphcanvas[0].canvas_mouse,this.scale=this.app.graph.list_of_graphcanvas[0].ds.scale),this.mouse[3]=this.color,this.mouse[4]=this.scale/10,this.canSendMouse){let e=Object.keys(this.nexusSocket.userManager.users);for(let t=0;t{if(t!=this.nexusSocket.uuid){let s=this.nexusSocket.userManager.get(t,"mouse"),n=this.nexusSocket.userManager.get(t,"name")||"User",o=this.nexusSocket.userManager.get(t,"pointer");if(this.nexusSocket.userManager.get(t,"mouse_hidden")==0&&(s=null),s&&s.length>0){let r=s[0]-4,h=s[1]-4,l=1-s[4];e.drawImage(o,r,h,18,18),e.font="14px Arial",e.fillStyle=s[3],e.fillText(n,r+22,h+16);let d=this.hex2rgb(s[3]);e.beginPath(),e.arc(r,h,l*600,0,2*Math.PI,!1),e.strokeStyle=`rgba(${d.r},${d.g},${d.b},0.1)`,e.lineWidth=2,e.stroke()}}})}};LiteGraph.use_uuids=!0;LiteGraph.uuidv4=function(){return Date.now()};LGraph.prototype.getGroupByUUID=function(i){let e=this._groups,t=null;for(let s=0;s=LiteGraph.MAX_NUMBER_OF_NODES)throw"LiteGraph: max number of nodes in a graph reached";if(LiteGraph.use_uuids){(i.id==null||i.id==-1)&&(i.id=LiteGraph.uuidv4());let t=localStorage.getItem("nexus-socket-uuid");t&&(i.properties.owner=t),this.last_node_id=i.id}else i.id==null||i.id==-1?i.id=++this.last_node_id:this.last_node_id=LiteGraph.MAX_NUMBER_OF_NODES)throw"LiteGraph: max number of nodes in a graph reached";return this.last_node_id=i.id,i.graph=this,this._version++,this._nodes.push(i),this._nodes_by_id[i.id]=i,this.config.align_to_grid&&i.alignToGrid(),e||this.updateExecutionOrder(),this.onNodeAddedNexus&&this.onNodeAddedNexus(i),this.setDirtyCanvas(!0),this.change(),i}};LGraph.prototype.remove=function(i){if(i.constructor===LiteGraph.LGraphGroup){var e=this._groups.indexOf(i);e!=-1&&this._groups.splice(e,1),i.graph=null,this._version++,this.setDirtyCanvas(!0,!0),this.change(),this.onGroupRemoved&&this.onGroupRemoved(i);return}if(this._nodes_by_id[i.id]!=null&&!i.ignore_remove){if(this.beforeChange(),i.inputs)for(var t=0;tthis.updateNode(e))};LGraph.prototype.updateLinks=function(i){i.forEach(e=>this.updateLink(e))};LGraph.prototype.updateGroups=function(i){i.forEach(e=>this.updateGroup(e))};var y="nexus";async function x(i,e={}){try{let t=await fetch(i,e),s=await t.json();if(!t.ok)throw new Error(s.error||"Something went wrong");return s}catch(t){return console.error(`Fetch error: ${t.message}`),{error:t.message}}}async function F(i,e){let t=`${y}/workflows/${e}`,s={method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({user_id:i})};return await x(t,s)}async function U(i){let e=`${y}/workflows/meta`,t={method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({user_id:i})};return await x(e,t)}async function D(i,e){let t=`${y}/workflow`,s={method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({user_id:i,...e})};return await x(t,s)}async function H(i,e){let t=`${y}/workflow/backup`,s={method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({user_id:i,...e})};return await x(t,s)}async function q(){let i=`${y}/workflow`;return await x(i)}async function O(i){let e=`${y}/permission/${i}`;return await x(e)}async function A(i,e,t,s){let n=`${y}/permission/${i}`,o={method:"POST",headers:{"Content-Type":"application/json",Authorization:s},body:JSON.stringify({admin_id:e,data:t})};return await x(n,o)}async function B(i,e,t){let s=`${y}/login`,n={method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({uuid:i,account:e,password:t})};return await x(s,n)}async function j(i){let e=`${y}/verify`,t={method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:i})};return await x(e,t)}var L=class{constructor(e,t,s){this.app=e,this.api=t,this.nexusSocket=s,this.nexusSocket.onMessageReceive.push(this.handleMessageReceive.bind(this)),this.init(),this.spectators=[],this.saveTime=0,setInterval(this.handleSpectators.bind(this),1e3/24)}async init(){let e=(await this.app.graphToPrompt()).workflow;await H(this.nexusSocket.uuid,e),this.app.graph.clear()}removeSaveManager(){this.saveTimer&&clearInterval(this.saveTimer)}async saveManager(e){this.nexusSocket.admin&&this.nexusSocket.sendMessage("workflow:timer",{time:e-2},!1),this.removeSaveManager(),this.saveTime=e,this.saveTimer=setInterval(this.sendWorkFlow.bind(this),e*1e3),console.log("Auto Backup Timer Setup:",e)}async sendWorkFlow(){let e=(await this.app.graphToPrompt()).workflow;await D(this.nexusSocket.uuid,e)}async loadLatestWorkflow(){let e=await q(),t=e.links||[],s=e.nodes||[],n=e.groups||[];this.app.graph.updateNodes(s),this.app.graph.updateLinks(t),this.app.graph.updateGroups(n),this.app.graph.updateExecutionOrder(),this.app.graph.setDirtyCanvas(!0,!0)}async handleSaveWorkflowManager(){this.sendWorkFlow()}async handleSpectators(){if(this.spectators.length>0){let t=(await this.app.graphToPrompt()).workflow.extra;for(let s=0;s{t.onGroupMoved=this.onGroupMoved.bind(this,t),t.onGroupNodeMoved=this.onNodeMoved.bind(this)}),this.app.graph._nodes.forEach(t=>{t.onWidgetChanged=this.onWidgetChanged.bind(this,t),t.onConnectionsChange=this.onNodeConnectionChange.bind(this,t)})):(this.app.graph.onNodeAddedNexus=null,this.app.graph.onNodeAdded=null,this.app.graph.onNodeRemoved=null,this.app.canvas.onSelectionChange=null,this.app.canvas.onNodeDeselected=null,this.app.canvas.onNodeMoved=null,this.app.graph.onGroupAdded=null,this.app.graph.onGroupRemoved=null,this.app.graph._groups.forEach(t=>{t.onGroupMoved=null,t.onGroupNodeMoved=null}),this.app.graph._nodes.forEach(t=>{t.onWidgetChanged=null,t.onConnectionsChange=null}))}handleWorkflowUpdate(e,t){let s=this.app.graph,n=this.app.canvas,o=t.update,a=t.data,r;switch(o){case"node-added":s.updateNode(a);break;case"node-removed":s.removeNode(a);break;case"node-moved":s.updateNode(a);break;case"node-widget-changed":s.updateNode(a);break;case"node-selection":let h=[];a.forEach(l=>{r=s.getNodeById(l.id),r&&h.push(r)}),n.selectNodesNexus(h,!0);break;case"node-deselection":r=s.getNodeById(a.id),r&&n.deselectNodeNexus(r);break;case"group-added":s.updateGroup(a);break;case"group-moved":s.updateGroup(a);break;case"group-removed":s.removeGroup(a);break;case"node-connection":s.updateLinkManual(t.change,t.link,t.node,s);break;default:break}}onNodeAddedNexus(e){e.onWidgetChanged=this.onWidgetChanged.bind(this,e),e.onConnectionsChange=this.onNodeConnectionChange.bind(this,e)}onNodeAdded(e){let t=e.serialize();e.onWidgetChanged=this.onWidgetChanged.bind(this,e),e.onConnectionsChange=this.onNodeConnectionChange.bind(this,e),this.nexusSocket.sendMessage("workflow:update",{update:"node-added",data:t},!1)}onWidgetChanged(e){let t=e.serialize();this.nexusSocket.sendMessage("workflow:update",{update:"node-widget-changed",data:t},!1)}onNodeRemoved(e){let t=e.serialize();this.nexusSocket.sendMessage("workflow:update",{update:"node-removed",data:t},!1)}async onNodeConnectionChange(e,t,s,n,o,a){let r=n?"add":"remove",h=o;this.graphData=(await this.app.graphToPrompt()).workflow;let l=this.graphData.links;e=this.app.graph.getNodeById(e.id),e=e?e.serialize():null,this.nexusSocket.sendMessage("workflow:update",{node:e,update:"node-connection",link:h,change:r,data:l},!1)}onSelectionChange(e){let t=[];Object.keys(e).forEach(s=>{t.push(e[s].serialize())}),this.nexusSocket.sendMessage("workflow:update",{update:"node-selection",data:t},!1)}onNodeDeselected(e){let t=e.serialize();this.nexusSocket.sendMessage("workflow:update",{update:"node-deselection",data:t},!1)}onNodeMoved(e){let t=e.serialize();this.nexusSocket.sendMessage("workflow:update",{update:"node-moved",data:t},!1)}onGroupAdded(e){let t=e.serialize(),s=localStorage.getItem("nexus-socket-uuid");s&&(t.owner=s,e.configure(t)),e.onGroupMoved=this.onGroupMoved.bind(this,e),e.onGroupNodeMoved=this.onNodeMoved.bind(this),this.nexusSocket.sendMessage("workflow:update",{update:"group-added",data:t},!1)}onGroupRemoved(e){let t=e.serialize();this.nexusSocket.sendMessage("workflow:update",{update:"group-removed",data:t},!1)}onGroupMoved(e){let t=e.serialize();this.nexusSocket.sendMessage("workflow:update",{update:"group-moved",data:t},!1)}};var G=class extends EventTarget{constructor(e,t,s){super(),this.app=t,this.api=e,this.nexusSocket=s,this.comfy_events=["b_preview","progress","executing","executed","execution_start","execution_error","execution_cached"];let n=e.queuePrompt;this.apiWrapper={oqp:n,enableApi:!1,api:e,qp:async function(o,a){return this.enableApi?this.oqp.call(this.api,o,{output:a.output,workflow:a.workflow}):{node_errors:{}}}},this.api.queuePrompt=this.apiWrapper.qp.bind(this.apiWrapper),this.nexusSocket.onMessageReceive.push(this.handleMessageReceive.bind(this)),this.addEventListeners(),this.comfy_events.forEach(o=>{this.api.addEventListener(o,this.handleComfyEvents.bind(this))}),this.handleProgressDraw(),this.handleControl(!1),this.handleEditor(!1)}handleProgressDraw(){let e=LGraphCanvas.prototype.drawNodeShape,t=this.app;LGraphCanvas.prototype.drawNodeShape=function(s,n,o,a,r,h,l){let d=e.apply(this,arguments),p=t.lastNodeErrors?.[s.id],u=null,m=1;if(s.id===t.runningNodeId?u="#0f0":t.dragOverNode&&s.id===t.dragOverNode.id?u="dodgerblue":p?.errors?(u="red",m=2):t.lastExecutionError&&t.lastExecutionError.node_id===s.id&&(u="#f0f",m=2),u){let f=s._shape||s.constructor.shape||LiteGraph.ROUND_SHAPE;n.lineWidth=m,n.globalAlpha=.8,n.beginPath(),f==LiteGraph.BOX_SHAPE?n.rect(-6,-6-LiteGraph.NODE_TITLE_HEIGHT,12+o[0]+1,12+o[1]+LiteGraph.NODE_TITLE_HEIGHT):f==LiteGraph.ROUND_SHAPE||f==LiteGraph.CARD_SHAPE&&s.flags.collapsed?n.roundRect(-6,-6-LiteGraph.NODE_TITLE_HEIGHT,12+o[0]+1,12+o[1]+LiteGraph.NODE_TITLE_HEIGHT,this.round_radius*2):f==LiteGraph.CARD_SHAPE?n.roundRect(-6,-6-LiteGraph.NODE_TITLE_HEIGHT,12+o[0]+1,12+o[1]+LiteGraph.NODE_TITLE_HEIGHT,[this.round_radius*2,this.round_radius*2,2,2]):f==LiteGraph.CIRCLE_SHAPE&&n.arc(o[0]*.5,o[1]*.5,o[0]*.5+6,0,Math.PI*2),n.strokeStyle=u,n.stroke(),n.strokeStyle=a,n.globalAlpha=1}if(t.progress&&s.id===t.runningNodeId&&(n.fillStyle="green",n.fillRect(0,0,o[0]*(t.progress.value/t.progress.max),6),n.fillStyle=r),p){n.lineWidth=2,n.strokeStyle="red";for(let f of p.errors)if(f.extra_info&&f.extra_info.input_name){let g=s.findInputSlot(f.extra_info.input_name);if(g!==-1){let b=s.getConnectionPos(!0,g);n.beginPath(),n.arc(b[0]-s.pos[0],b[1]-s.pos[1],12,0,2*Math.PI,!1),n.stroke()}}}return d}}handleMessageReceive(e){try{this.dispatchEvent(new CustomEvent(e.name,{detail:e}))}catch(t){console.error("Error handling message:",t)}}handleComfyEvents(e){e.type==="b_preview"?this.blobToBase64(e.detail).then(t=>{this.nexusSocket.sendMessage(e.type,{detail:t},!1)}):this.nexusSocket.sendMessage(e.type,{detail:e.detail},!1)}addEventListeners(){this.addEventListener("progress",({detail:e})=>{e=e.data.detail,this.app.progress=e,this.app.graph.setDirtyCanvas(!0,!1)}),this.addEventListener("executing",({detail:e})=>{e=e.data.detail,this.app.progress=null,this.app.runningNodeId=e,this.app.graph.setDirtyCanvas(!0,!1),delete this.app.nodePreviewImages[this.app.runningNodeId]}),this.addEventListener("executed",({detail:e})=>{e=e.data.detail;let t=this.app.nodeOutputs[e.node];if(e.merge&&t)for(let n in e.output??{}){let o=t[n];o instanceof Array?t[n]=o.concat(e.output[n]):t[n]=e.output[n]}else this.app.nodeOutputs[e.node]=e.output;let s=this.app.graph.getNodeById(e.node);s&&s.onExecuted&&s.onExecuted(e.output)}),this.addEventListener("execution_start",({detail:e})=>{e=e.data.detail,this.app.runningNodeId=null,this.app.lastExecutionError=null,this.app.graph._nodes.forEach(t=>{t.onExecutionStart&&t.onExecutionStart()})}),this.addEventListener("execution_error",({detail:e})=>{e=e.data.detail,this.app.lastExecutionError=e;let t=this.formatExecutionError(e);this.app.ui.dialog.show(t),this.app.canvas.draw(!0,!0)}),this.addEventListener("b_preview",({detail:e})=>{e=e.data.detail;let t=this.app.runningNodeId;if(t==null)return;let s=this.base64ToBlob(e),n=URL.createObjectURL(s);this.app.nodePreviewImages[t]=[n]})}blobToBase64(e){return new Promise((t,s)=>{let n=new FileReader;n.onloadend=()=>{t(n.result)},n.onerror=s,n.readAsDataURL(e)})}base64ToBlob(e){let[t,s]=e.split(","),n=t.match(/:(.*?);/)[1],o=atob(s),a=new Array(o.length);for(let h=0;h{e==="display"&&(this.defaultDisplay=t),this.el.style[e]=t})}removeStyles(){this.el&&(Object.keys(this.styles).forEach(e=>{this.el.style[e]=""}),this.styles={})}updateAttrs(e={}){this.attrs={...this.attrs,...e},this.addAttrs()}addAttrs(){this.el&&Object.entries(this.attrs).forEach(([e,t])=>{this.el.setAttribute(e,t)})}removeAttrs(){this.el&&(Object.keys(this.attrs).forEach(e=>{this.el.removeAttribute(e)}),this.attrs={})}addChildrens(){this.el&&(this.childrens.forEach(e=>{typeof e=="string"?this.el.insertAdjacentHTML("beforeend",e):e instanceof Node?this.el.appendChild(e):e instanceof v?this.el.appendChild(e.el):e instanceof i&&this.el.appendChild(e.el)}),this.afterChildrenAppend&&requestAnimationFrame(()=>this.afterChildrenAppend(this.childrens,this)))}addChild(e){this.el&&(typeof e=="string"?this.el.insertAdjacentHTML("beforeend",e):e instanceof Node?this.el.appendChild(e):e instanceof i&&this.el.appendChild(e.el),this.childrens.includes(e)||this.childrens.push(e),this.afterChildrenAppend&&this.afterChildrenAppend(this.childrens,this))}removeChildrens(){this.el&&(this.el.innerHTML="",this.childrens=[])}addEvents(){this.el&&Object.entries(this.events).forEach(([e,t])=>{this.el.addEventListener(e,s=>{t(s,this.parent)})})}removeEvents(){this.el&&(Object.entries(this.events).forEach(([e,t])=>{this.el.removeEventListener(e,t)}),this.events={})}show(){this.el&&(this.el.style.display=this.defaultDisplay,this.visible=!0)}hide(){this.el&&(this.el.style.display="none",this.visible=!1)}addStateStyles(){this.el&&(this.el.addEventListener("mouseenter",()=>this.applyStyles(this.hoverStyles)),this.el.addEventListener("mouseleave",()=>this.applyStyles(this.styles)),this.el.addEventListener("focus",()=>this.applyStyles(this.focusStyles),!0),this.el.addEventListener("blur",()=>this.applyStyles(this.styles),!0),this.el.addEventListener("mousedown",()=>this.applyStyles(this.activeStyles)),this.el.addEventListener("mouseup",()=>this.applyStyles(this.styles)))}applyStyles(e){this.visible&&Object.entries(e).forEach(([t,s])=>{this.el.style[t]=s})}};var z=new c({id:"nexus-chat-main",tag:"div",visible:!0,styles:{width:"512px",minHeight:"170px",maxHeight:"170px",overflow:"hidden",padding:"16px",borderRadius:"8px",border:"1px solid rgba(94,94,94,0.5)"},hoverStyles:{overflowY:"scroll"},afterChildrenAppend:function(i,e){i.length>50&&(i[0].removeElement(),i.shift()),e.el.scrollTop=e.el.scrollHeight}}),_=new c({id:"nexus-input-main",tag:"div",visible:!0,styles:{width:"100%",height:"32px",display:"flex",alignItems:"center"},childrens:[new c({id:"nexus-input-label",tag:"span",visible:!1,styles:{fontFamily:'Calibri, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',display:"block",fontSize:"14px",lineHeight:"14px",fontWeight:600,color:"white",padding:"0",margin:"0",textShadow:"1pt 1pt #000"},childrens:["Say:"]}),new c({id:"nexus-input",tag:"input",visible:!1,styles:{fontFamily:'Calibri, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',fontSize:"14px",lineHeight:"14px",fontWeight:600,width:"100%",color:"white",padding:"0",margin:"0",border:"none",background:"transparent",flexGrow:1,outline:"none",textIndent:"4pt",caretColor:"transparent",textShadow:"1pt 1pt #000"}})]}),C=new c({id:"nexus-chat-window",tag:"div",styles:{position:"absolute",left:"16px",top:"100px",display:"flex",flexDirection:"column",zIndex:1e3},childrens:[new c({id:"nexus-total-players",tag:"div",visible:!0,styles:{fontFamily:'Calibri, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',fontSize:"14px",lineHeight:"14px",fontWeight:600,left:"16px",top:"80px",color:"#a5a5a5",width:"100%",paddingBottom:"12px",textAlign:"right"},childrens:["Users Online: ",new c({id:"nexus-total-players-count",visible:!0,tag:"span",childrens:["0"]})]}),z,_,new v({styles:{"#nexus-chat-main::-webkit-scrollbar":{display:"none","scrollbar-width":"none","-ms-overflow-style":"none"},"#nexus-chat-window":{}}})]}),w=function(i,e,t,s,n){let o="";n?o=`${i} ${e}`:o=`${i}: ${e}`;let a=new c({tag:"span",visible:!0,styles:{fontFamily:'Calibri, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',display:"block",fontSize:"14px",lineHeight:"14px",fontWeight:600,width:"100%",color:"white",padding:"0",margin:"0",textShadow:"1pt 1pt #000",marginBottom:"1pt",lineBreak:"anywhere"},childrens:[o]});z.addChild(a)};var I=class{constructor(){this.active=0,this.panel=new c({id:"nexus-player-panel",tag:"div",visible:!1,styles:{width:"100%",height:"100vh",display:"flex","align-items":"center","justify-content":"center","box-sizing":"border-box",position:"absolute",left:"0",top:"0","z-index":"10000"},childrens:[new c({id:"nexus-player-panel-table",tag:"div",visible:!0,styles:{width:"832px",display:"flex","flex-direction":"column","border-radius":"6px","box-sizing":"border-box"},childrens:[new c({id:"nexus-player-panel-table-body",tag:"div",visible:!0,styles:{width:"100%",display:"flex","flex-direction":"column","align-items":"center","justify-content":"flex-start",color:"#a3a3a3","font-family":"Calibri","font-weight":"600",gap:"12px","padding-bottom":"12px","box-sizing":"border-box",background:"rgba(54,54,54,0.8)","border-radius":"6px","box-shadow":"4px 8px 8px rgba(0,0,0,0.5)","max-height":"540px",overflowY:"auto"}}),new v({styles:{"#nexus-player-panel .table-row":{width:"100%",display:"grid","align-items":"center","grid-template-columns":"20% 60% 20%","justify-content":"space-between"},"#nexus-player-panel .table-row:first-child":{width:"100%",color:"#a3a3a3",padding:"16px 0",background:"rgb(0,0,0,0.6)","border-top-left-radius":"6px","border-top-right-radius":"6px",position:"sticky",top:"0"},"#nexus-player-panel .table-row .table-row-child":{display:"flex","justify-content":"flex-start",gap:"12px","padding-left":"12px",width:"100%"},"#nexus-player-panel .table-row-child button":{border:"none",padding:"6px 12px","border-radius":"3px",cursor:"pointer",background:"rgba(94,94,94)",color:"#a1a1a1"},"#nexus-player-panel .table-row-child button:active":{opacity:"0.8"},"#nexus-player-panel .table-row-child button[active='1'][label='spectate']":{background:"#218c74",color:"#fafafa"},"#nexus-player-panel .table-row-child button[active='1'][label='editor']":{background:"#474787",color:"#fafafa"},"#nexus-player-panel .table-row-child button[active='1'][label='mouse']":{background:"#cd6133",color:"#fafafa"},"#nexus-player-panel .table-row-child button[active='1'][label='queue']":{background:"#6F1E51",color:"#fafafa"}}})]})]}),this.onCommandReceived=null,this.buttons=[],this.columns=[]}addColumnsToPanel(e){this.columns=e}addRowsToPanel(e,t,s,n){let o=this.panel.childrens[0].childrens[0];o.removeChildrens(),this.buttons=[];let a=document.createElement("div");a.className="table-row",this.columns.forEach(r=>{let h=document.createElement("span");h.className="table-row-child",h.textContent=r,a.appendChild(h)}),o.addChild(a),e.forEach((r,h)=>{let l=document.createElement("div");l.className="table-row",r.forEach((d,p)=>{let u=document.createElement("span");u.className="table-row-child";let m=d.indexOf(",")>-1,f=d.indexOf(":")>-1,g=[];m?(g=d.split(","),g.forEach((b,k)=>{let S=b.split(":");this.addChildToParent(S,u,t,s,n,h,p,k)})):f?(g=d.split(":"),this.addChildToParent(g,u,t,s,n,h,p,-1)):u.textContent=d,l.appendChild(u)}),o.addChild(l)})}addChildToParent(e,t,s,n,o,a,r,h){switch(e[0]){case"button":let l=document.createElement("button");if(e[1]=="none"&&(l.className="none"),this.buttons.includes(l)||this.buttons.push(l),l.textContent=e[1],s[a]&&s[a][r]&&n[a][r]&&o[a][r]){let d=n[a][r].split(","),p=o[a][r].split(",");h!=-1?(l.setAttribute("data",p[h]),l.setAttribute("active",d[h])):(l.setAttribute("data",o[a][r]),l.setAttribute("active",n[a][r])),l.setAttribute("kind",e[0]),l.setAttribute("label",e[1]),e[1]!="none"&&(l.onclick=s[a][r].bind(this))}t.appendChild(l);break}}disableButtons(e){this.buttons.forEach(t=>{let s=t.getAttribute("kind"),n=t.getAttribute("data"),o=parseInt(t.getAttribute("active")),a=t.getAttribute("label");a==e&&o==1&&(t.setAttribute("active",0),this.onCommandReceived&&this.onCommandReceived(s,a,n,0))})}onclick(e){let t=e.target.getAttribute("kind"),s=e.target.getAttribute("data"),n=e.target.getAttribute("label"),o=parseInt(e.target.getAttribute("active"));t=="button"&&(n=="spectate"&&this.disableButtons(n),o==0?e.target.setAttribute("active",1):o==1&&e.target.setAttribute("active",0)),o=parseInt(e.target.getAttribute("active")),this.onCommandReceived&&this.onCommandReceived(t,n,s,o)}};var P=class{constructor(){this.active=0,this.panel=new c({id:"nexus-workflow-panel",tag:"div",visible:!1,styles:{width:"100%",height:"100vh",display:"flex","align-items":"center","justify-content":"center","box-sizing":"border-box",position:"absolute",left:"0",top:"0","z-index":"10000"},childrens:[new c({id:"nexus-workflow-panel-table",tag:"div",visible:!0,styles:{width:"512px",display:"flex","flex-direction":"column","border-radius":"6px","box-sizing":"border-box"},childrens:[new c({id:"nexus-workflow-panel-table-body",tag:"div",visible:!0,styles:{width:"100%",display:"flex","flex-direction":"column","align-items":"center","justify-content":"flex-start",color:"#a3a3a3","font-family":"Calibri","font-weight":"600",gap:"12px","padding-bottom":"12px","box-sizing":"border-box",background:"rgba(54,54,54,0.8)","border-radius":"6px","box-shadow":"4px 8px 8px rgba(0,0,0,0.5)","max-height":"540px",overflowY:"auto"}}),new v({styles:{"#nexus-workflow-panel .table-row":{width:"100%",display:"flex","align-items":"center","justify-content":"center"},"#nexus-workflow-panel .table-row:first-child":{width:"100%",color:"#a3a3a3",padding:"16px 0",background:"rgb(0,0,0,0.6)","border-top-left-radius":"6px","border-top-right-radius":"6px",position:"sticky",top:"0"},"#nexus-workflow-panel .table-row .table-row-child":{display:"flex","justify-content":"flex-start",gap:"12px","padding-left":"12px",width:"100%"},"#nexus-workflow-panel .table-row-child button":{border:"none",padding:"6px 12px","border-radius":"3px",cursor:"pointer",background:"rgba(94,94,94)",color:"#a1a1a1",width:"100%"},"#nexus-workflow-panel .table-row-child:first-child":{width:"50%"},"#nexus-workflow-panel .table-row-child:last-child":{"padding-right":"12px"},"#nexus-workflow-panel .table-row-child button:active":{opacity:"0.8"},"#nexus-workflow-panel .table-row-child button[label='load']:active":{background:"#218c74",color:"#fafafa"},"#nexus-workflow-panel .table-row-child button[label='loadforall']:active":{background:"#474787",color:"#fafafa"}}})]})]}),this.onCommandReceived=null,this.buttons=[],this.columns=[]}addColumnsToPanel(e){this.columns=e}addRowsToPanel(e,t,s,n){let o=this.panel.childrens[0].childrens[0];o.removeChildrens(),this.buttons=[];let a=document.createElement("div");a.className="table-row",this.columns.forEach(r=>{let h=document.createElement("span");h.className="table-row-child",h.textContent=r,a.appendChild(h)}),o.addChild(a),e.forEach((r,h)=>{let l=document.createElement("div");l.className="table-row",r.forEach((d,p)=>{let u=document.createElement("span");u.className="table-row-child";let m=d.indexOf(",")>-1,f=d.indexOf(":")>-1,g=[];m?(g=d.split(","),g.forEach((b,k)=>{let S=b.split(":");this.addChildToParent(S,u,t,s,n,h,p,k)})):f?(g=d.split(":"),this.addChildToParent(g,u,t,s,n,h,p,-1)):(d=d.split("|"),d=d.join(":"),u.textContent=d),l.appendChild(u)}),o.addChild(l)})}addChildToParent(e,t,s,n,o,a,r,h){switch(e[0]){case"button":let l=document.createElement("button");if(e[1]=="none"&&(l.className="none"),this.buttons.includes(l)||this.buttons.push(l),l.textContent=e[1],s[a]&&s[a][r]&&n[a][r]&&o[a][r]){let d=n[a][r].split(","),p=o[a][r].split(",");h!=-1?(l.setAttribute("data",p[h]),l.setAttribute("active",d[h])):(l.setAttribute("data",o[a][r]),l.setAttribute("active",n[a][r])),l.setAttribute("kind",e[0]),l.setAttribute("label",e[1]),e[1]!="none"&&(l.onclick=s[a][r].bind(this))}t.appendChild(l);break}}onclick(e){let t=e.target.getAttribute("kind"),s=e.target.getAttribute("data"),n=e.target.getAttribute("label"),o=parseInt(e.target.getAttribute("active"));t=="button"&&(o==0?e.target.setAttribute("active",1):o==1&&e.target.setAttribute("active",0)),o=parseInt(e.target.getAttribute("active")),this.onCommandReceived&&this.onCommandReceived(t,n,s,o)}};var R=class{constructor(e,t,s,n,o,a,r){this.color=n,this.colors={join:"#ff6e4d",info:"#fba510",personal:"#fba510",name:"#00fa00",none:"#ffffff"},this.inputMain=_,this.nexusSocket=s,this.app=e,this.api=t,this.workflowManager=a,this.promptControl=r,this.mouseManger=o,this.nexusSocket.onConnected.push(this.handleConnect.bind(this)),this.nexusSocket.onMessageReceive.push(this.handleMessageReceive.bind(this)),this.inputMain.addEventListener("message",this.handleMessage.bind(this)),document.addEventListener("keydown",this.handleChatWindow.bind(this)),this.disableComfyThings=this.disableComfyThings.bind(this),this.enableComfyThings=this.enableComfyThings.bind(this),this.disableGraphSelectionWithExtremePrejudice=this.disableGraphSelectionWithExtremePrejudice.bind(this),this.comfyThings=["#extraOptions","#queue-button","#comfy-save-button","#comfy-file-input","#comfy-clear-button","#comfy-load-default-button",".queue-tab-button.side-bar-button",".queue-tab-button.side-bar-button","#comfy-refresh-button","#queue-front-button","#comfy-view-queue-button","#comfy-view-history-button"],C.childrens[3].softUpdate("#nexus-chat-window","pointer-events","none"),C.show(),setInterval(h=>{h.childrens[0].childrens[1].el.textContent=Object.keys(this.nexusSocket.userManager.users).length},1e3,C),this.up=new I,this.up.onCommandReceived=this.onPanelCommandReceived.bind(this),this.wp=new P,this.wp.onCommandReceived=this.onWFPanelCommandReceived.bind(this),this.spectating=null,this.lastkey=null}timeAgo(e){let t=new Date,s=new Date(e),n=Math.floor((t-s)/1e3),o=Math.floor(n/60),a=Math.floor(o/60),r=Math.floor(a/24);return o<1?"just now":o===1?"1 minute ago":o<60?`${o} minutes ago`:a===1?"1 hour ago":a<24?`${a} hours ago`:r===1?"1 day ago":`${r} days ago`}async verifyLogin(){this.disableComfyThings();let e=localStorage.getItem("nexus-socket-token");if(e){let t=await j(e);t&&this.nexusSocket.uuid==t.user_id?(this.enableComfyThings(),this.promptControl.handleControl(!0),this.promptControl.handleEditor(!0),this.workflowManager.handleWorkflowEvents(!0),this.nexusSocket.admin=!0,w("","You are now an admin!",this.color.info,this.colors.personal,!0),this.workflowManager.saveManager(60)):(this.promptControl.handleControl(!1),this.promptControl.handleEditor(!1),this.workflowManager.handleWorkflowEvents(!1),this.workflowManager.removeSaveManager(),w("","You are a viewer!",this.color.info,this.colors.info,!0))}else{let t=await O(this.nexusSocket.uuid);if(t){let s=t.editor?t.editor:!1,n=t.queue?t.queue:!1;this.workflowManager.handleWorkflowEvents(s),this.promptControl.handleEditor(s),this.promptControl.handleControl(n),this.nexusSocket.queue=n,this.nexusSocket.editor=s,s?this.workflowManager.saveManager(60):this.workflowManager.removeSaveManager(),w("",`Permission: Workflow Editor ${s?"Yes":"No"} | Queue Prompt ${n?"Yes":"No"} `,this.color.info,this.colors.personal,!0),n&&w("","You can now queue prompt press CTRL+Enter",this.color.info,this.colors.personal,!0)}}this.workflowManager.loadLatestWorkflow()}handleConnect(){let e=this.nexusSocket.userManager.get(this.nexusSocket.uuid,"name");this.nexusSocket.sendMessage("chat-join",{name:e},!1),this.verifyLogin()}findChildrenAfterHr(e){let t=document.querySelector(`.${e}`),s=[];if(t){let n=t.querySelector("hr");if(n){let o=n.nextElementSibling;for(;o;)s.push(o),o=o.nextElementSibling}}return s}disableGraphSelectionWithExtremePrejudice(){!this.nexusSocket.admin&&!this.nexusSocket.editor&&this.app.graph&&this.app.graph.list_of_graphcanvas&&this.app.graph.list_of_graphcanvas.length>0&&this.app.graph.list_of_graphcanvas[0].deselectAllNodes()}disableComfyThings(){this.comfyThings.forEach(t=>{document.querySelectorAll(t).forEach(n=>{n.disabled=!0,n.style.pointerEvents="none"})}),document.querySelectorAll(".comfy-settings-btn").forEach(t=>{t.disabled=!0,t.style.pointerEvents="none"}),this.findChildrenAfterHr("comfy-menu").forEach(t=>{(t.tagName==="BUTTON"||t.tagName==="INPUT"||t.tagName==="SELECT"||t.tagName==="TEXTAREA")&&(t.disabled=!0)}),document.querySelector(".comfy-menu").style.display="none",document.querySelector(".comfy-menu").style.pointerEvents="none",clearInterval(this.disableComfyThingsTimer),this.disableComfyThingsTimer=setInterval(()=>{this.comfyThings.forEach(s=>{document.querySelectorAll(s).forEach(o=>{o.disabled=!0,o.style.pointerEvents="none"})}),document.querySelectorAll(".comfy-settings-btn").forEach(s=>{s.disabled=!0,s.style.pointerEvents="none"}),this.findChildrenAfterHr("comfy-menu").forEach(s=>{(s.tagName==="BUTTON"||s.tagName==="INPUT"||s.tagName==="SELECT"||s.tagName==="TEXTAREA")&&(s.disabled=!0)}),document.querySelector(".comfy-menu").style.display="none",document.querySelector(".comfy-menu").style.pointerEvents="none",this.promptControl.handleEditor(this.nexusSocket.editor?this.nexusSocket.editor:!1),this.promptControl.handleControl(this.nexusSocket.queue?this.nexusSocket.queue:!1)},500),this.disableGraphSelectionWithExtremePrejudiceTimer=setInterval(this.disableGraphSelectionWithExtremePrejudice,50)}enableComfyThings(){clearInterval(this.disableComfyThingsTimer),clearInterval(this.disableGraphSelectionWithExtremePrejudiceTimer),this.comfyThings.forEach(t=>{document.querySelectorAll(t).forEach(n=>{n.disabled=!1,n.style.pointerEvents="all"})}),document.querySelectorAll(".comfy-settings-btn").forEach(t=>{t.disabled=!1,t.style.pointerEvents="all"}),this.findChildrenAfterHr("comfy-menu").forEach(t=>{(t.tagName==="BUTTON"||t.tagName==="INPUT"||t.tagName==="SELECT"||t.tagName==="TEXTAREA")&&(t.disabled=!1)}),document.querySelector(".comfy-menu").style.display="flex",document.querySelector(".comfy-menu").style.pointerEvents="all"}handleChatWindow(e){let t=e.key,s=_.childrens[0],n=_.childrens[1],o={tag:document.activeElement.tagName,el:document.activeElement,chat:document.activeElement==n.el};function a(){!["INPUT","TEXTAREA"].includes(o.tag)&&n.visible==!1&&(e.preventDefault(),s.show(),n.show(),n.el.focus(),n.el.value="",n.focusTimer=setInterval((d,p)=>{document.activeElement!=p&&d.visible==!0&&p.focus()},500,n,n.el),C.childrens[3].softUpdate("#nexus-chat-window","pointer-events","all"))}function r(){o.chat&&(s.hide(),n.el.blur(),n.el.value="",n.hide(),clearTimeout(n.focusTimer),C.childrens[3].softUpdate("#nexus-chat-window","pointer-events","none"))}function h(){if(!o.chat)return;let l=n.el.value;_.dispatchEvent(new CustomEvent("message",{detail:{message:l}})),r()}switch(t){case"t":case"T":a(),this.togglePlayerTab(!0),this.toggleWFTab(!0);break;case"Escape":r();break;case"Enter":h();break;case"p":case"P":e.altKey&&e.shiftKey&&(o.chat||this.togglePlayerTab());break;case"o":case"O":e.altKey&&e.shiftKey&&(o.chat||this.toggleWFTab());break}return o}toggleWFTab(e,t){(this.nexusSocket.admin||this.nexusSocket.editor)&&(e?(this.wp.active=0,this.wp.panel.hide()):t?this.prepareWFPanel():this.wp.active==0?(this.wp.active=1,this.wp.panel.show(),this.prepareWFPanel()):this.wp.active==1&&(this.wp.active=0,this.wp.panel.hide()),this.wp.active==1?this.workflowManager.removeSaveManager():this.workflowManager.saveManager(this.workflowManager.saveTime||60))}togglePlayerTab(e,t){e?(this.up.active=0,this.up.panel.hide()):t?this.preparePanel(this.nexusSocket.admin):this.up.active==0?(this.up.active=1,this.up.panel.show(),this.preparePanel(this.nexusSocket.admin)):this.up.active==1&&(this.up.active=0,this.up.panel.hide())}async onWFPanelCommandReceived(e,t,s,n){switch(e){case"button":if(t=="load"){let o=s,a=await F(this.nexusSocket.uuid,o);if(a){a.clear=!0;let r={name:"workflow",from:this.nexusSocket.uuid,data:{workflow:a}};this.nexusSocket.onMessageReceive.forEach(h=>{h(r)}),this.nexusSocket.admin&&this.nexusSocket.sendMessage("workflow",{workflow:a},!1)}}break}}async onPanelCommandReceived(e,t,s,n){switch(e){case"button":if(t=="spectate")this.nexusSocket.sendMessage("spectate",{on:n,receiver:s},!1),n==1?this.spectating=s:this.spectating=null;else if(t=="editor"){if(this.nexusSocket.admin){let o=localStorage.getItem("nexus-socket-token");await A(s,this.nexusSocket.uuid,{editor:!!n},o),this.nexusSocket.sendMessage("permission",{receiver:s},!1)}}else if(t=="queue"){if(this.nexusSocket.admin){let o=localStorage.getItem("nexus-socket-token");await A(s,this.nexusSocket.uuid,{queue:!!n},o),this.nexusSocket.sendMessage("permission",{receiver:s},!1)}}else t=="mouse"&&(this.nexusSocket.sendMessage("mouse_view_toggle",{on:n,receiver:s},!1),this.nexusSocket.userManager.set(s,"hide_mouse",n));break}}async prepareWFPanel(){let e=["Name","Created","Action"];this.wp.addColumnsToPanel(e);let t=await U(this.nexusSocket.uuid),s=[],n=[],o=[],a=[];for(let r=0;r]+(>|$)/g;e=e.replace(a,"");for(let r of e)n.test(r)&&!o.test(r)||(s+=r);return s}}startsWithCommand(e){return/^\/\w+(\s+\S+)?/.test(e)}extractCommandAndValue(e){let t=/^\/(\w+)\s*(.*)/,s=e.match(t);if(s){let n=s[1],o=s[2].trim()?s[2].split(/\s+/):[];return{command:n,values:o}}else return null}};var J=window.comfyAPI.app.app,W=window.comfyAPI.api.api,E=new N,Y={name:"ComfyUI-Nexus",async setup(i){i=i;let e=new T(E,i,24),t=new L(i,W,E),s=new G(W,i,E);new R(i,W,E,e.color,e,t,s),E.connect()},async beforeConfigureGraph(){}};J.registerExtension(Y);})(); +let nexus = { + name: "ComfyUI-Nexus", + async setup(app) { + app = app + let mouseManager = new NexusMouseManager(nexusSocket, app, 10) + let workflowManager = new NexusWorkflowManager(app, api, nexusSocket) + let nexusPromptControl = new NexusPromptControl(api, app, nexusSocket) + new NexusCommands(app, api, nexusSocket, mouseManager.color, mouseManager, workflowManager, nexusPromptControl) + nexusSocket.connect() + }, + async beforeConfigureGraph() { + } +} + +app.registerExtension(nexus); \ No newline at end of file