Merge branch 'main' into fix-notice-board-padding
This commit is contained in:
@@ -1,23 +1,61 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js"
|
||||
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
||||
import { ShareDialog, SUPPORTED_OUTPUT_NODE_TYPES, getPotentialOutputsAndOutputNodes, ShareDialogChooser, showOpenArtShareDialog, showShareDialog } from "./comfyui-share-common.js";
|
||||
import {
|
||||
ShareDialog,
|
||||
SUPPORTED_OUTPUT_NODE_TYPES,
|
||||
getPotentialOutputsAndOutputNodes,
|
||||
ShareDialogChooser,
|
||||
showOpenArtShareDialog,
|
||||
showShareDialog,
|
||||
showYouMLShareDialog
|
||||
} from "./comfyui-share-common.js";
|
||||
import { OpenArtShareDialog } from "./comfyui-share-openart.js";
|
||||
import { CustomNodesInstaller } from "./custom-nodes-downloader.js";
|
||||
import { AlternativesInstaller } from "./a1111-alter-downloader.js";
|
||||
import { SnapshotManager } from "./snapshot.js";
|
||||
import { ModelInstaller } from "./model-downloader.js";
|
||||
import { manager_instance, setManagerInstance, install_via_git_url, install_pip, rebootAPI, free_models } from "./common.js";
|
||||
import { manager_instance, setManagerInstance, install_via_git_url, install_pip, rebootAPI, free_models } from "./common.js";
|
||||
import { ComponentBuilderDialog, load_components, set_component_policy, getPureName } from "./components-manager.js";
|
||||
|
||||
var docStyle = document.createElement('style');
|
||||
docStyle.innerHTML = `
|
||||
#cm-manager-dialog {
|
||||
width: 1000px;
|
||||
height: 465px;
|
||||
height: 495px;
|
||||
box-sizing: content-box;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.cb-widget {
|
||||
width: 400px;
|
||||
height: 25px;
|
||||
box-sizing: border-box;
|
||||
z-index: 10000;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.cb-widget-input {
|
||||
width: 305px;
|
||||
height: 25px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.cb-widget-input:disabled {
|
||||
background-color: #444444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cb-widget-input-label {
|
||||
width: 90px;
|
||||
height: 25px;
|
||||
box-sizing: border-box;
|
||||
color: white;
|
||||
text-align: right;
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.cm-menu-container {
|
||||
column-gap: 20px;
|
||||
display: flex;
|
||||
@@ -63,6 +101,39 @@ docStyle.innerHTML = `
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#custom-nodes-grid a {
|
||||
color: #5555FF;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#custom-nodes-grid a:hover {
|
||||
color: #7777FF;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#external-models-grid a {
|
||||
color: #5555FF;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#external-models-grid a:hover {
|
||||
color: #7777FF;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#alternatives-grid a {
|
||||
color: #5555FF;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#alternatives-grid a:hover {
|
||||
color: #7777FF;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.cm-notice-board {
|
||||
width: 290px;
|
||||
height: 230px;
|
||||
@@ -220,6 +291,16 @@ const style = `
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cb-node-label {
|
||||
width: 400px;
|
||||
height:28px;
|
||||
color: black;
|
||||
background-color: #777777;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#cm-close-button {
|
||||
width: calc(100% - 65px);
|
||||
bottom: 10px;
|
||||
@@ -227,6 +308,16 @@ const style = `
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#cm-save-button {
|
||||
width: calc(100% - 65px);
|
||||
bottom:40px;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
}
|
||||
#cm-save-button:disabled {
|
||||
background-color: #444444;
|
||||
}
|
||||
|
||||
.pysssss-workflow-arrow-2 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -327,6 +418,15 @@ function getNickname(node, nodename) {
|
||||
if (nicknames[nodename]) {
|
||||
node.nickname = nicknames[nodename];
|
||||
}
|
||||
else if(node.getInnerNodes) {
|
||||
let pure_name = getPureName(node);
|
||||
let groupNode = app.graph.extra?.groupNodes?.[pure_name];
|
||||
if(groupNode) {
|
||||
let packname = groupNode.packname;
|
||||
node.nickname = packname;
|
||||
}
|
||||
return node.nickname;
|
||||
}
|
||||
else {
|
||||
for(let i in nickname_patterns) {
|
||||
let item = nickname_patterns[i];
|
||||
@@ -641,6 +741,18 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
}
|
||||
}),
|
||||
|
||||
$el("button.cm-button", {
|
||||
type: "button",
|
||||
textContent: "Install via Git URL",
|
||||
onclick: () => {
|
||||
var url = prompt("Please enter the URL of the Git repository to install", "");
|
||||
|
||||
if (url !== null) {
|
||||
install_via_git_url(url, self);
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
$el("br", {}, []),
|
||||
update_all_button,
|
||||
update_comfyui_button,
|
||||
@@ -673,6 +785,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
|
||||
// db mode
|
||||
this.datasrc_combo = document.createElement("select");
|
||||
this.datasrc_combo.setAttribute("title", "Configure where to retrieve node/model information. If set to 'local,' the channel is ignored, and if set to 'channel (remote),' it fetches the latest information each time the list is opened.");
|
||||
this.datasrc_combo.className = "cm-menu-combo";
|
||||
this.datasrc_combo.appendChild($el('option', { value: 'cache', text: 'DB: Channel (1day cache)' }, []));
|
||||
this.datasrc_combo.appendChild($el('option', { value: 'local', text: 'DB: Local' }, []));
|
||||
@@ -680,6 +793,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
|
||||
// preview method
|
||||
let preview_combo = document.createElement("select");
|
||||
preview_combo.setAttribute("title", "Configure how latent variables will be decoded during preview in the sampling process.");
|
||||
preview_combo.className = "cm-menu-combo";
|
||||
preview_combo.appendChild($el('option', { value: 'auto', text: 'Preview method: Auto' }, []));
|
||||
preview_combo.appendChild($el('option', { value: 'taesd', text: 'Preview method: TAESD (slow)' }, []));
|
||||
@@ -688,7 +802,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
|
||||
api.fetchApi('/manager/preview_method')
|
||||
.then(response => response.text())
|
||||
.then(data => { preview_combo.value = data; })
|
||||
.then(data => { preview_combo.value = data; });
|
||||
|
||||
preview_combo.addEventListener('change', function (event) {
|
||||
api.fetchApi(`/manager/preview_method?value=${event.target.value}`);
|
||||
@@ -696,6 +810,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
|
||||
// nickname
|
||||
let badge_combo = document.createElement("select");
|
||||
badge_combo.setAttribute("title", "Configure the content to be displayed on the badge at the top right corner of the node. The ID is the identifier of the node. If 'hide built-in' is selected, both unknown nodes and built-in nodes will be omitted, making them indistinguishable");
|
||||
badge_combo.className = "cm-menu-combo";
|
||||
badge_combo.appendChild($el('option', { value: 'none', text: 'Badge: None' }, []));
|
||||
badge_combo.appendChild($el('option', { value: 'nick', text: 'Badge: Nickname' }, []));
|
||||
@@ -715,6 +830,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
|
||||
// channel
|
||||
let channel_combo = document.createElement("select");
|
||||
channel_combo.setAttribute("title", "Configure the channel for retrieving data from the Custom Node list (including missing nodes) or the Model list. Note that the badge utilizes local information.");
|
||||
channel_combo.className = "cm-menu-combo";
|
||||
api.fetchApi('/manager/channel_url_list')
|
||||
.then(response => response.json())
|
||||
@@ -739,12 +855,30 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
}
|
||||
});
|
||||
|
||||
// default ui state
|
||||
let default_ui_combo = document.createElement("select");
|
||||
default_ui_combo.setAttribute("title", "Set the default state to be displayed in the main menu when the browser starts.");
|
||||
default_ui_combo.className = "cm-menu-combo";
|
||||
default_ui_combo.appendChild($el('option', { value: 'none', text: 'Default UI: None' }, []));
|
||||
default_ui_combo.appendChild($el('option', { value: 'history', text: 'Default UI: History' }, []));
|
||||
default_ui_combo.appendChild($el('option', { value: 'queue', text: 'Default UI: Queue' }, []));
|
||||
api.fetchApi('/manager/default_ui')
|
||||
.then(response => response.text())
|
||||
.then(data => { default_ui_combo.value = data; });
|
||||
|
||||
default_ui_combo.addEventListener('change', function (event) {
|
||||
api.fetchApi(`/manager/default_ui?value=${event.target.value}`);
|
||||
});
|
||||
|
||||
|
||||
// share
|
||||
let share_combo = document.createElement("select");
|
||||
share_combo.setAttribute("title", "Hide the share button in the main menu or set the default action upon clicking it. Additionally, configure the default share site when sharing via the context menu's share button.");
|
||||
share_combo.className = "cm-menu-combo";
|
||||
const share_options = [
|
||||
['none', 'None'],
|
||||
['openart', 'OpenArt AI'],
|
||||
['youml', 'YouML'],
|
||||
['matrix', 'Matrix Server'],
|
||||
['comfyworkflows', 'ComfyWorkflows'],
|
||||
['all', 'All'],
|
||||
@@ -753,6 +887,25 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
share_combo.appendChild($el('option', { value: option[0], text: `Share: ${option[1]}` }, []));
|
||||
}
|
||||
|
||||
// default ui state
|
||||
let component_policy_combo = document.createElement("select");
|
||||
component_policy_combo.setAttribute("title", "When loading the workflow, configure which version of the component to use.");
|
||||
component_policy_combo.className = "cm-menu-combo";
|
||||
component_policy_combo.appendChild($el('option', { value: 'workflow', text: 'Component: Use workflow version' }, []));
|
||||
component_policy_combo.appendChild($el('option', { value: 'higher', text: 'Component: Use higher version' }, []));
|
||||
component_policy_combo.appendChild($el('option', { value: 'mine', text: 'Component: Use my version' }, []));
|
||||
api.fetchApi('/manager/component/policy')
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
component_policy_combo.value = data;
|
||||
set_component_policy(data);
|
||||
});
|
||||
|
||||
component_policy_combo.addEventListener('change', function (event) {
|
||||
api.fetchApi(`/manager/component/policy?value=${event.target.value}`);
|
||||
set_component_policy(event.target.value);
|
||||
});
|
||||
|
||||
api.fetchApi('/manager/share_option')
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
@@ -779,19 +932,10 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
channel_combo,
|
||||
preview_combo,
|
||||
badge_combo,
|
||||
default_ui_combo,
|
||||
share_combo,
|
||||
component_policy_combo,
|
||||
$el("br", {}, []),
|
||||
$el("button.cm-button", {
|
||||
type: "button",
|
||||
textContent: "Install via Git URL",
|
||||
onclick: () => {
|
||||
var url = prompt("Please enter the URL of the Git repository to install", "");
|
||||
|
||||
if (url !== null) {
|
||||
install_via_git_url(url, self);
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
$el("br", {}, []),
|
||||
$el("filedset.cm-experimental", {}, [
|
||||
@@ -825,7 +969,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
})
|
||||
]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
createControlsRight() {
|
||||
const elts = [
|
||||
@@ -877,12 +1021,12 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
]),
|
||||
|
||||
$el("button", {
|
||||
id: 'workflowgallery-button',
|
||||
type: "button",
|
||||
id: 'workflowgallery-button',
|
||||
type: "button",
|
||||
style: {
|
||||
...(localStorage.getItem("wg_last_visited") ? {height: '50px'} : {})
|
||||
...(localStorage.getItem("wg_last_visited") ? {height: '50px'} : {})
|
||||
},
|
||||
onclick: (e) => {
|
||||
onclick: (e) => {
|
||||
const last_visited_site = localStorage.getItem("wg_last_visited")
|
||||
if (!!last_visited_site) {
|
||||
window.open(last_visited_site, "comfyui-workflow-gallery");
|
||||
@@ -892,15 +1036,15 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
},
|
||||
}, [
|
||||
$el("p", {
|
||||
textContent: 'Workflow Gallery',
|
||||
style: {
|
||||
'text-align': 'center',
|
||||
'color': 'white',
|
||||
'font-size': '18px',
|
||||
'margin': 0,
|
||||
'padding': 0,
|
||||
}
|
||||
}, [
|
||||
textContent: 'Workflow Gallery',
|
||||
style: {
|
||||
'text-align': 'center',
|
||||
'color': 'white',
|
||||
'font-size': '18px',
|
||||
'margin': 0,
|
||||
'padding': 0,
|
||||
}
|
||||
}, [
|
||||
$el("p", {
|
||||
id: 'workflowgallery-button-last-visited-label',
|
||||
textContent: `(${localStorage.getItem("wg_last_visited") ? localStorage.getItem("wg_last_visited").split('/')[2] : ''})`,
|
||||
@@ -913,10 +1057,10 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
}
|
||||
})
|
||||
]),
|
||||
$el("div.pysssss-workflow-arrow-2", {
|
||||
id: `comfyworkflows-button-arrow`,
|
||||
onclick: this.handleWorkflowGalleryButtonClick
|
||||
})
|
||||
$el("div.pysssss-workflow-arrow-2", {
|
||||
id: `comfyworkflows-button-arrow`,
|
||||
onclick: this.handleWorkflowGalleryButtonClick
|
||||
})
|
||||
]),
|
||||
|
||||
$el("button.cm-button", {
|
||||
@@ -971,10 +1115,10 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
this.element.style.display = "block";
|
||||
}
|
||||
|
||||
handleWorkflowGalleryButtonClick(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
LiteGraph.closeAllContextMenus();
|
||||
handleWorkflowGalleryButtonClick(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
LiteGraph.closeAllContextMenus();
|
||||
|
||||
// Modify the style of the button so that the UI can indicate the last
|
||||
// visited site right away.
|
||||
@@ -985,60 +1129,72 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
lastVisitedLabel.textContent = `(${url.split('/')[2]})`;
|
||||
}
|
||||
|
||||
const menu = new LiteGraph.ContextMenu(
|
||||
[
|
||||
{
|
||||
title: "Share your art",
|
||||
callback: () => {
|
||||
if (share_option === 'openart') {
|
||||
showOpenArtShareDialog();
|
||||
return;
|
||||
} else if (share_option === 'matrix' || share_option === 'comfyworkflows') {
|
||||
showShareDialog(share_option);
|
||||
return;
|
||||
}
|
||||
const menu = new LiteGraph.ContextMenu(
|
||||
[
|
||||
{
|
||||
title: "Share your art",
|
||||
callback: () => {
|
||||
if (share_option === 'openart') {
|
||||
showOpenArtShareDialog();
|
||||
return;
|
||||
} else if (share_option === 'matrix' || share_option === 'comfyworkflows') {
|
||||
showShareDialog(share_option);
|
||||
return;
|
||||
} else if (share_option === 'youml') {
|
||||
showYouMLShareDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ShareDialogChooser.instance) {
|
||||
ShareDialogChooser.instance = new ShareDialogChooser();
|
||||
}
|
||||
ShareDialogChooser.instance.show();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Open 'openart.ai'",
|
||||
if (!ShareDialogChooser.instance) {
|
||||
ShareDialogChooser.instance = new ShareDialogChooser();
|
||||
}
|
||||
ShareDialogChooser.instance.show();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Open 'openart.ai'",
|
||||
callback: () => {
|
||||
const url = "https://openart.ai/workflows/dev";
|
||||
localStorage.setItem("wg_last_visited", url);
|
||||
window.open(url, "comfyui-workflow-gallery");
|
||||
modifyButtonStyle(url);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Open 'comfyworkflows.com'",
|
||||
callback: () => {
|
||||
},
|
||||
{
|
||||
title: "Open 'youml.com'",
|
||||
callback: () => {
|
||||
const url = "https://youml.com/?from=comfyui-share";
|
||||
localStorage.setItem("wg_last_visited", url);
|
||||
window.open(url, "comfyui-workflow-gallery");
|
||||
modifyButtonStyle(url);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Open 'comfyworkflows.com'",
|
||||
callback: () => {
|
||||
const url = "https://comfyworkflows.com/";
|
||||
localStorage.setItem("wg_last_visited", url);
|
||||
window.open(url, "comfyui-workflow-gallery");
|
||||
window.open(url, "comfyui-workflow-gallery");
|
||||
modifyButtonStyle(url);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Close",
|
||||
callback: () => {
|
||||
LiteGraph.closeAllContextMenus();
|
||||
},
|
||||
}
|
||||
],
|
||||
{
|
||||
event: e,
|
||||
scale: 1.3,
|
||||
},
|
||||
window
|
||||
);
|
||||
// set the id so that we can override the context menu's z-index to be above the comfyui manager menu
|
||||
menu.root.id = "workflowgallery-button-menu";
|
||||
menu.root.classList.add("pysssss-workflow-popup-2");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Close",
|
||||
callback: () => {
|
||||
LiteGraph.closeAllContextMenus();
|
||||
},
|
||||
}
|
||||
],
|
||||
{
|
||||
event: e,
|
||||
scale: 1.3,
|
||||
},
|
||||
window
|
||||
);
|
||||
// set the id so that we can override the context menu's z-index to be above the comfyui manager menu
|
||||
menu.root.id = "workflowgallery-button-menu";
|
||||
menu.root.classList.add("pysssss-workflow-popup-2");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1051,6 +1207,14 @@ app.registerExtension({
|
||||
});
|
||||
},
|
||||
async setup() {
|
||||
let orig_clear = app.graph.clear;
|
||||
app.graph.clear = function () {
|
||||
orig_clear.call(app.graph);
|
||||
load_components();
|
||||
};
|
||||
|
||||
load_components();
|
||||
|
||||
const menu = document.querySelector(".comfy-menu");
|
||||
const separator = document.createElement("hr");
|
||||
|
||||
@@ -1078,6 +1242,9 @@ app.registerExtension({
|
||||
} else if (share_option === 'matrix' || share_option === 'comfyworkflows') {
|
||||
showShareDialog(share_option);
|
||||
return;
|
||||
} else if (share_option === 'youml') {
|
||||
showYouMLShareDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
if(!ShareDialogChooser.instance) {
|
||||
@@ -1100,6 +1267,7 @@ app.registerExtension({
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
this._addExtraNodeContextMenu(nodeType, app);
|
||||
},
|
||||
|
||||
async nodeCreated(node, app) {
|
||||
if(!node.badge_enabled) {
|
||||
node.getNickname = function () { return getNickname(node, node.comfyClass.trim()) };
|
||||
@@ -1110,6 +1278,7 @@ app.registerExtension({
|
||||
node.badge_enabled = true;
|
||||
}
|
||||
},
|
||||
|
||||
async loadedGraphNode(node, app) {
|
||||
if(!node.badge_enabled) {
|
||||
const orig = node.onDrawForeground;
|
||||
@@ -1120,8 +1289,23 @@ app.registerExtension({
|
||||
|
||||
_addExtraNodeContextMenu(node, app) {
|
||||
const origGetExtraMenuOptions = node.prototype.getExtraMenuOptions;
|
||||
node.prototype.cm_menu_added = true;
|
||||
node.prototype.getExtraMenuOptions = function (_, options) {
|
||||
origGetExtraMenuOptions?.apply?.(this, arguments);
|
||||
|
||||
if (node.category.startsWith('group nodes/')) {
|
||||
options.push({
|
||||
content: "Save As Component",
|
||||
callback: (obj) => {
|
||||
if (!ComponentBuilderDialog.instance) {
|
||||
ComponentBuilderDialog.instance = new ComponentBuilderDialog();
|
||||
}
|
||||
ComponentBuilderDialog.instance.target_node = node;
|
||||
ComponentBuilderDialog.instance.show();
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
|
||||
if (isOutputNode(node)) {
|
||||
const { potential_outputs } = getPotentialOutputsAndOutputNodes([this]);
|
||||
const hasOutput = potential_outputs.length > 0;
|
||||
@@ -1158,3 +1342,27 @@ app.registerExtension({
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
async function set_default_ui()
|
||||
{
|
||||
let res = await api.fetchApi('/manager/default_ui');
|
||||
if(res.status == 200) {
|
||||
let mode = await res.text();
|
||||
switch(mode) {
|
||||
case 'history':
|
||||
app.ui.queue.hide();
|
||||
app.ui.history.show();
|
||||
break;
|
||||
case 'queue':
|
||||
app.ui.queue.show();
|
||||
app.ui.history.hide();
|
||||
break;
|
||||
default:
|
||||
// do nothing
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set_default_ui();
|
||||
@@ -2,6 +2,7 @@ import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
||||
import { OpenArtShareDialog } from "./comfyui-share-openart.js";
|
||||
import { YouMLShareDialog } from "./comfyui-share-youml.js";
|
||||
|
||||
export const SUPPORTED_OUTPUT_NODE_TYPES = [
|
||||
"PreviewImage",
|
||||
@@ -179,6 +180,23 @@ export const showOpenArtShareDialog = () => {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export const showYouMLShareDialog = () => {
|
||||
if (!YouMLShareDialog.instance) {
|
||||
YouMLShareDialog.instance = new YouMLShareDialog();
|
||||
}
|
||||
|
||||
return app.graphToPrompt()
|
||||
.then(prompt => {
|
||||
return app.graph._nodes;
|
||||
})
|
||||
.then(nodes => {
|
||||
const { potential_outputs, potential_output_nodes } = getPotentialOutputsAndOutputNodes(nodes);
|
||||
YouMLShareDialog.instance.show(potential_outputs, potential_output_nodes);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export const showShareDialog = async (share_option) => {
|
||||
if (!ShareDialog.instance) {
|
||||
ShareDialog.instance = new ShareDialog(share_option);
|
||||
@@ -233,6 +251,16 @@ export class ShareDialogChooser extends ComfyDialog {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "youml",
|
||||
textContent: "YouML",
|
||||
website: "https://youml.com",
|
||||
description: "Share your workflow or transform it into an interactive app on YouML.com",
|
||||
onclick: () => {
|
||||
showYouMLShareDialog();
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "matrix",
|
||||
textContent: "Matrix Server",
|
||||
@@ -264,7 +292,7 @@ export class ShareDialogChooser extends ComfyDialog {
|
||||
display: "flex",
|
||||
'flex-wrap': 'wrap',
|
||||
'justify-content': 'space-around',
|
||||
'padding': '20px',
|
||||
'padding': '10px',
|
||||
}
|
||||
});
|
||||
|
||||
@@ -297,7 +325,7 @@ export class ShareDialogChooser extends ComfyDialog {
|
||||
'text-align': 'left',
|
||||
color: 'white',
|
||||
'font-size': '14px',
|
||||
'margin-bottom': '10px',
|
||||
'margin-bottom': '0',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -335,7 +363,7 @@ export class ShareDialogChooser extends ComfyDialog {
|
||||
style: {
|
||||
'flex-basis': '100%',
|
||||
'margin': '10px',
|
||||
'padding': '20px',
|
||||
'padding': '10px 20px',
|
||||
'border': '1px solid #ddd',
|
||||
'border-radius': '5px',
|
||||
'box-shadow': '0 2px 4px rgba(0, 0, 0, 0.1)',
|
||||
|
||||
@@ -264,9 +264,10 @@ export class OpenArtShareDialog extends ComfyDialog {
|
||||
]);
|
||||
|
||||
// OpenArt Contest Section
|
||||
/*
|
||||
this.joinContestCheckbox = $el("input", {
|
||||
type: 'checkbox',
|
||||
id: "join_contest"
|
||||
id: "join_contest"s
|
||||
}, [])
|
||||
this.joinContestDescription = $el("a", {
|
||||
style: {
|
||||
@@ -290,6 +291,7 @@ export class OpenArtShareDialog extends ComfyDialog {
|
||||
const contestSection = $el("div", {style: sectionStyle}, [
|
||||
this.joinContestLabel,
|
||||
]);
|
||||
*/
|
||||
|
||||
// Message Section
|
||||
this.message = $el(
|
||||
@@ -349,7 +351,7 @@ export class OpenArtShareDialog extends ComfyDialog {
|
||||
outputUploadSection,
|
||||
this.outputsSection,
|
||||
additionalInputsSection,
|
||||
contestSection,
|
||||
// contestSection,
|
||||
this.message,
|
||||
buttonsSection,
|
||||
];
|
||||
@@ -486,7 +488,7 @@ export class OpenArtShareDialog extends ComfyDialog {
|
||||
}
|
||||
}
|
||||
|
||||
const join_contest = this.joinContestCheckbox.checked;
|
||||
// const join_contest = this.joinContestCheckbox.checked;
|
||||
|
||||
try {
|
||||
const response = await this.fetchApi(
|
||||
@@ -502,7 +504,7 @@ export class OpenArtShareDialog extends ComfyDialog {
|
||||
workflow_api_json: workflowAPIJSON,
|
||||
snapshot: current_snapshot,
|
||||
},
|
||||
join_contest,
|
||||
// join_contest,
|
||||
}),
|
||||
},
|
||||
"Uploading workflow..."
|
||||
|
||||
568
js/comfyui-share-youml.js
Normal file
568
js/comfyui-share-youml.js
Normal file
@@ -0,0 +1,568 @@
|
||||
import {app} from "../../scripts/app.js";
|
||||
import {api} from "../../scripts/api.js";
|
||||
import {ComfyDialog, $el} from "../../scripts/ui.js";
|
||||
|
||||
const BASE_URL = "https://youml.com";
|
||||
//const BASE_URL = "http://localhost:3000";
|
||||
const DEFAULT_HOMEPAGE_URL = `${BASE_URL}/?from=comfyui`;
|
||||
const TOKEN_PAGE_URL = `${BASE_URL}/my-token`;
|
||||
const API_ENDPOINT = `${BASE_URL}/api`;
|
||||
|
||||
const style = `
|
||||
.youml-share-dialog {
|
||||
overflow-y: auto;
|
||||
}
|
||||
.youml-share-dialog .dialog-header {
|
||||
text-align: center;
|
||||
color: white;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
.youml-share-dialog .dialog-section {
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.youml-share-dialog input, .youml-share-dialog textarea {
|
||||
display: block;
|
||||
min-width: 500px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.youml-share-dialog textarea {
|
||||
color: var(--input-text);
|
||||
background-color: var(--comfy-input-bg);
|
||||
}
|
||||
.youml-share-dialog .workflow-description {
|
||||
min-height: 75px;
|
||||
}
|
||||
.youml-share-dialog label {
|
||||
color: #f8f8f8;
|
||||
display: block;
|
||||
margin: 5px 0 0 0;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
.youml-share-dialog .action-button {
|
||||
padding: 10px 80px;
|
||||
margin: 10px 5px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.youml-share-dialog .share-button {
|
||||
color: #fff;
|
||||
background-color: #007bff;
|
||||
}
|
||||
.youml-share-dialog .close-button {
|
||||
background-color: none;
|
||||
}
|
||||
.youml-share-dialog .action-button-panel {
|
||||
text-align: right;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.youml-share-dialog .status-message {
|
||||
color: #fd7909;
|
||||
text-align: center;
|
||||
padding: 5px;
|
||||
font-size: 18px;
|
||||
}
|
||||
.youml-share-dialog .status-message a {
|
||||
color: white;
|
||||
}
|
||||
.youml-share-dialog .output-panel {
|
||||
overflow: auto;
|
||||
max-height: 180px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
grid-template-rows: auto;
|
||||
grid-column-gap: 10px;
|
||||
grid-row-gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
.youml-share-dialog .output-panel .output-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
objectFit: cover;
|
||||
borderRadius: 5px;
|
||||
}
|
||||
|
||||
.youml-share-dialog .output-panel .radio-button {
|
||||
color:var(--fg-color);
|
||||
}
|
||||
.youml-share-dialog .output-panel .radio-text {
|
||||
color: gray;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
max-width: 100px;
|
||||
}
|
||||
.youml-share-dialog .output-panel .node-id {
|
||||
color: #FBFBFD;
|
||||
display: block;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
font-size: 12px;
|
||||
overflow-x: hidden;
|
||||
padding: 2px 3px;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
max-width: 100px;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.youml-share-dialog .output-panel .output-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
border: 5px solid transparent;
|
||||
}
|
||||
.youml-share-dialog .output-panel .output-label:hover {
|
||||
border: 5px solid #007bff;
|
||||
}
|
||||
.youml-share-dialog .output-panel .output-label.checked {
|
||||
border: 5px solid #007bff;
|
||||
}
|
||||
.youml-share-dialog .missing-output-message{
|
||||
color: #fd7909;
|
||||
font-size: 16px;
|
||||
margin-bottom:10px
|
||||
}
|
||||
.youml-share-dialog .select-output-message{
|
||||
color: white;
|
||||
margin-bottom:5px
|
||||
}
|
||||
`;
|
||||
|
||||
export class YouMLShareDialog extends ComfyDialog {
|
||||
static instance = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
$el("style", {
|
||||
textContent: style,
|
||||
parent: document.head,
|
||||
});
|
||||
this.element = $el(
|
||||
"div.comfy-modal.youml-share-dialog",
|
||||
{
|
||||
parent: document.body,
|
||||
},
|
||||
[$el("div.comfy-modal-content", {}, [...this.createLayout()])]
|
||||
);
|
||||
this.selectedOutputIndex = 0;
|
||||
this.selectedNodeId = null;
|
||||
this.uploadedImages = [];
|
||||
this.selectedFile = null;
|
||||
}
|
||||
|
||||
async loadToken() {
|
||||
let key = ""
|
||||
try {
|
||||
const response = await api.fetchApi(`/manager/youml/settings`)
|
||||
const settings = await response.json()
|
||||
return settings.token
|
||||
} catch (error) {
|
||||
}
|
||||
return key || "";
|
||||
}
|
||||
|
||||
async saveToken(value) {
|
||||
await api.fetchApi(`/manager/youml/settings`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
token: value
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
createLayout() {
|
||||
// Header Section
|
||||
const headerSection = $el("h3.dialog-header", {
|
||||
textContent: "Share your workflow to YouML.com",
|
||||
size: 3,
|
||||
});
|
||||
|
||||
// Workflow Info Section
|
||||
this.nameInput = $el("input", {
|
||||
type: "text",
|
||||
placeholder: "Name (required)",
|
||||
});
|
||||
this.descriptionInput = $el("textarea.workflow-description", {
|
||||
placeholder: "Description (optional, markdown supported)",
|
||||
});
|
||||
const workflowMetadata = $el("div.dialog-section", {}, [
|
||||
$el("label", {}, ["Workflow info"]),
|
||||
this.nameInput,
|
||||
this.descriptionInput,
|
||||
]);
|
||||
|
||||
// Outputs Section
|
||||
this.outputsSection = $el("div.dialog-section", {
|
||||
id: "selectOutputs",
|
||||
}, []);
|
||||
|
||||
const outputUploadSection = $el("div.dialog-section", {}, [
|
||||
$el("label", {}, ["Thumbnail"]),
|
||||
this.outputsSection,
|
||||
]);
|
||||
|
||||
// API Token Section
|
||||
this.apiTokenInput = $el("input", {
|
||||
type: "password",
|
||||
placeholder: "Copy & paste your API token",
|
||||
});
|
||||
const getAPITokenButton = $el("button", {
|
||||
href: DEFAULT_HOMEPAGE_URL,
|
||||
target: "_blank",
|
||||
onclick: () => window.open(TOKEN_PAGE_URL, "_blank"),
|
||||
}, ["Get your API Token"])
|
||||
|
||||
const apiTokenSection = $el("div.dialog-section", {}, [
|
||||
$el("label", {}, ["YouML API Token"]),
|
||||
this.apiTokenInput,
|
||||
getAPITokenButton,
|
||||
]);
|
||||
|
||||
// Message Section
|
||||
this.message = $el("div.status-message", {}, []);
|
||||
|
||||
// Share and Close Buttons
|
||||
this.shareButton = $el("button.action-button.share-button", {
|
||||
type: "submit",
|
||||
textContent: "Share",
|
||||
onclick: () => {
|
||||
this.handleShareButtonClick();
|
||||
},
|
||||
});
|
||||
|
||||
const buttonsSection = $el(
|
||||
"div.action-button-panel",
|
||||
{},
|
||||
[
|
||||
$el("button.action-button.close-button", {
|
||||
type: "button",
|
||||
textContent: "Close",
|
||||
onclick: () => {
|
||||
this.close();
|
||||
},
|
||||
}),
|
||||
this.shareButton,
|
||||
]
|
||||
);
|
||||
|
||||
// Composing the full layout
|
||||
const layout = [
|
||||
headerSection,
|
||||
workflowMetadata,
|
||||
outputUploadSection,
|
||||
apiTokenSection,
|
||||
this.message,
|
||||
buttonsSection,
|
||||
];
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
async fetchYoumlApi(path, options, statusText) {
|
||||
if (statusText) {
|
||||
this.message.textContent = statusText;
|
||||
}
|
||||
|
||||
const fullPath = new URL(API_ENDPOINT + path)
|
||||
|
||||
const fetchOptions = Object.assign({}, options)
|
||||
|
||||
fetchOptions.headers = {
|
||||
...fetchOptions.headers,
|
||||
"Authorization": `Bearer ${this.apiTokenInput.value}`,
|
||||
"User-Agent": "ComfyUI-Manager-Youml/1.0.0",
|
||||
}
|
||||
|
||||
const response = await fetch(fullPath, fetchOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText + " " + (await response.text()));
|
||||
}
|
||||
|
||||
if (statusText) {
|
||||
this.message.textContent = "";
|
||||
}
|
||||
const data = await response.json();
|
||||
return {
|
||||
ok: response.ok,
|
||||
statusText: response.statusText,
|
||||
status: response.status,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
async uploadThumbnail(uploadFile, recipeId) {
|
||||
const form = new FormData();
|
||||
form.append("file", uploadFile, uploadFile.name);
|
||||
try {
|
||||
const res = await this.fetchYoumlApi(
|
||||
`/v1/comfy/recipes/${recipeId}/thumbnail`,
|
||||
{
|
||||
method: "POST",
|
||||
body: form,
|
||||
},
|
||||
"Uploading thumbnail..."
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
if (e?.response?.status === 413) {
|
||||
throw new Error("File size is too large (max 20MB)");
|
||||
} else {
|
||||
throw new Error("Error uploading thumbnail: " + e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleShareButtonClick() {
|
||||
this.message.textContent = "";
|
||||
await this.saveToken(this.apiTokenInput.value);
|
||||
try {
|
||||
this.shareButton.disabled = true;
|
||||
this.shareButton.textContent = "Sharing...";
|
||||
await this.share();
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
} finally {
|
||||
this.shareButton.disabled = false;
|
||||
this.shareButton.textContent = "Share";
|
||||
}
|
||||
}
|
||||
|
||||
async share() {
|
||||
const prompt = await app.graphToPrompt();
|
||||
const workflowJSON = prompt["workflow"];
|
||||
const workflowAPIJSON = prompt["output"];
|
||||
const form_values = {
|
||||
name: this.nameInput.value,
|
||||
description: this.descriptionInput.value,
|
||||
};
|
||||
|
||||
if (!this.apiTokenInput.value) {
|
||||
throw new Error("API token is required");
|
||||
}
|
||||
|
||||
if (!this.selectedFile) {
|
||||
throw new Error("Thumbnail is required");
|
||||
}
|
||||
|
||||
if (!form_values.name) {
|
||||
throw new Error("Title is required");
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
let snapshotData = null;
|
||||
try {
|
||||
const snapshot = await api.fetchApi(`/snapshot/get_current`)
|
||||
snapshotData = await snapshot.json()
|
||||
} catch (e) {
|
||||
console.error("Failed to get snapshot", e)
|
||||
}
|
||||
|
||||
const request = {
|
||||
name: this.nameInput.value,
|
||||
description: this.descriptionInput.value,
|
||||
workflowUiJson: JSON.stringify(workflowJSON),
|
||||
workflowApiJson: JSON.stringify(workflowAPIJSON),
|
||||
}
|
||||
|
||||
if (snapshotData) {
|
||||
request.snapshotJson = JSON.stringify(snapshotData)
|
||||
}
|
||||
|
||||
const response = await this.fetchYoumlApi(
|
||||
"/v1/comfy/recipes",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify(request),
|
||||
},
|
||||
"Uploading workflow..."
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const {id, recipePageUrl, editorPageUrl} = response.data;
|
||||
if (id) {
|
||||
let messagePrefix = "Workflow has been shared."
|
||||
if (this.selectedFile) {
|
||||
try {
|
||||
await this.uploadThumbnail(this.selectedFile, id);
|
||||
} catch (e) {
|
||||
console.error("Thumbnail upload failed: ", e);
|
||||
messagePrefix = "Workflow has been shared, but thumbnail upload failed. You can create a thumbnail on YouML later."
|
||||
}
|
||||
}
|
||||
this.message.innerHTML = `${messagePrefix} To turn your workflow into an interactive app, ` +
|
||||
`<a href="${recipePageUrl}" target="_blank">visit it on YouML</a>`;
|
||||
|
||||
this.uploadedImages = [];
|
||||
this.nameInput.value = "";
|
||||
this.descriptionInput.value = "";
|
||||
this.radioButtons.forEach((ele) => {
|
||||
ele.checked = false;
|
||||
ele.parentElement.classList.remove("checked");
|
||||
});
|
||||
this.selectedOutputIndex = 0;
|
||||
this.selectedNodeId = null;
|
||||
this.selectedFile = null;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error("Error sharing workflow: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchImageBlob(url) {
|
||||
const response = await fetch(url);
|
||||
const blob = await response.blob();
|
||||
return blob;
|
||||
}
|
||||
|
||||
async show(potentialOutputs, potentialOutputNodes) {
|
||||
const potentialOutputsToOrder = {};
|
||||
potentialOutputNodes.forEach((node, index) => {
|
||||
if (node.id in potentialOutputsToOrder) {
|
||||
potentialOutputsToOrder[node.id][1].push(potentialOutputs[index]);
|
||||
} else {
|
||||
potentialOutputsToOrder[node.id] = [node, [potentialOutputs[index]]];
|
||||
}
|
||||
})
|
||||
const sortedPotentialOutputsToOrder = Object.fromEntries(
|
||||
Object.entries(potentialOutputsToOrder).sort((a, b) => a[0].id - b[0].id)
|
||||
);
|
||||
const sortedPotentialOutputs = []
|
||||
const sortedPotentiaOutputNodes = []
|
||||
for (const [key, value] of Object.entries(sortedPotentialOutputsToOrder)) {
|
||||
sortedPotentiaOutputNodes.push(value[0]);
|
||||
sortedPotentialOutputs.push(...value[1]);
|
||||
}
|
||||
potentialOutputNodes = sortedPotentiaOutputNodes;
|
||||
potentialOutputs = sortedPotentialOutputs;
|
||||
|
||||
|
||||
// If `selectedNodeId` is provided, we will select the corresponding radio
|
||||
// button for the node. In addition, we move the selected radio button to
|
||||
// the top of the list.
|
||||
if (this.selectedNodeId) {
|
||||
const index = potentialOutputNodes.findIndex(node => node.id === this.selectedNodeId);
|
||||
if (index >= 0) {
|
||||
this.selectedOutputIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
this.radioButtons = [];
|
||||
const newRadioButtons = $el("div.output-panel",
|
||||
{
|
||||
id: "selectOutput-Options",
|
||||
},
|
||||
potentialOutputs.map((output, index) => {
|
||||
const {node_id: nodeId} = output;
|
||||
const radioButton = $el("input.radio-button", {
|
||||
type: "radio",
|
||||
name: "selectOutputImages",
|
||||
value: index,
|
||||
required: index === 0
|
||||
}, [])
|
||||
let radioButtonImage;
|
||||
let filename;
|
||||
if (output.type === "image" || output.type === "temp") {
|
||||
radioButtonImage = $el("img.output-image", {
|
||||
src: `/view?filename=${output.image.filename}&subfolder=${output.image.subfolder}&type=${output.image.type}`,
|
||||
}, []);
|
||||
filename = output.image.filename
|
||||
} else if (output.type === "output") {
|
||||
radioButtonImage = $el("img.output-image", {
|
||||
src: output.output.value,
|
||||
}, []);
|
||||
filename = output.output.filename
|
||||
} else {
|
||||
radioButtonImage = $el("img.output-image", {
|
||||
src: "",
|
||||
}, []);
|
||||
}
|
||||
const radioButtonText = $el("span.radio-text", {}, [output.title])
|
||||
const nodeIdChip = $el("span.node-id", {}, [`Node: ${nodeId}`])
|
||||
radioButton.checked = this.selectedOutputIndex === index;
|
||||
|
||||
radioButton.onchange = async () => {
|
||||
this.selectedOutputIndex = parseInt(radioButton.value);
|
||||
|
||||
// Remove the "checked" class from all radio buttons
|
||||
this.radioButtons.forEach((ele) => {
|
||||
ele.parentElement.classList.remove("checked");
|
||||
});
|
||||
radioButton.parentElement.classList.add("checked");
|
||||
|
||||
this.fetchImageBlob(radioButtonImage.src).then((blob) => {
|
||||
const file = new File([blob], filename, {
|
||||
type: blob.type,
|
||||
});
|
||||
this.selectedFile = file;
|
||||
})
|
||||
};
|
||||
|
||||
if (radioButton.checked) {
|
||||
this.fetchImageBlob(radioButtonImage.src).then((blob) => {
|
||||
const file = new File([blob], filename, {
|
||||
type: blob.type,
|
||||
});
|
||||
this.selectedFile = file;
|
||||
})
|
||||
}
|
||||
|
||||
this.radioButtons.push(radioButton);
|
||||
|
||||
return $el(`label.output-label${radioButton.checked ? '.checked' : ''}`, {},
|
||||
[radioButtonImage, radioButtonText, radioButton, nodeIdChip]);
|
||||
})
|
||||
);
|
||||
|
||||
let header;
|
||||
if (this.radioButtons.length === 0) {
|
||||
header = $el("div.missing-output-message", {textContent: "Queue Prompt to see the outputs and select a thumbnail"}, [])
|
||||
} else {
|
||||
header = $el("div.select-output-message", {textContent: "Choose one from the outputs (scroll to see all)"}, [])
|
||||
}
|
||||
|
||||
this.outputsSection.innerHTML = "";
|
||||
this.outputsSection.appendChild(header);
|
||||
if (this.radioButtons.length > 0) {
|
||||
this.outputsSection.appendChild(newRadioButtons);
|
||||
}
|
||||
|
||||
this.message.innerHTML = "";
|
||||
this.message.textContent = "";
|
||||
|
||||
const token = await this.loadToken();
|
||||
this.apiTokenInput.value = token;
|
||||
this.uploadedImages = [];
|
||||
|
||||
this.element.style.display = "block";
|
||||
}
|
||||
}
|
||||
13
js/common.js
13
js/common.js
@@ -1,5 +1,5 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js"
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
export async function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
@@ -48,8 +48,7 @@ export async function install_checked_custom_node(grid_rows, target_i, caller, m
|
||||
});
|
||||
|
||||
if(response.status == 400) {
|
||||
app.ui.dialog.show(`${mode} failed: ${target.title}`);
|
||||
app.ui.dialog.element.style.zIndex = 10010;
|
||||
show_message(`${mode} failed: ${target.title}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -64,8 +63,7 @@ export async function install_checked_custom_node(grid_rows, target_i, caller, m
|
||||
}
|
||||
|
||||
if(failed != '') {
|
||||
app.ui.dialog.show(`${mode} failed: ${failed}`);
|
||||
app.ui.dialog.element.style.zIndex = 10010;
|
||||
show_message(`${mode} failed: ${failed}`);
|
||||
}
|
||||
|
||||
await caller.invalidateControl();
|
||||
@@ -160,4 +158,9 @@ export async function free_models() {
|
||||
app.ui.dialog.show('Unloading of models failed.<BR><BR>Installed ComfyUI may be an outdated version.')
|
||||
}
|
||||
app.ui.dialog.element.style.zIndex = 10010;
|
||||
}
|
||||
|
||||
export function show_message(msg) {
|
||||
app.ui.dialog.show(msg);
|
||||
app.ui.dialog.element.style.zIndex = 10010;
|
||||
}
|
||||
801
js/components-manager.js
Normal file
801
js/components-manager.js
Normal file
@@ -0,0 +1,801 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js"
|
||||
import { sleep, show_message } from "./common.js";
|
||||
import { GroupNodeConfig, GroupNodeHandler } from "../../extensions/core/groupNode.js";
|
||||
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
||||
|
||||
let pack_map = {};
|
||||
let rpack_map = {};
|
||||
|
||||
export function getPureName(node) {
|
||||
// group nodes/
|
||||
let category = null;
|
||||
if(node.category) {
|
||||
category = node.category.substring(12);
|
||||
}
|
||||
else {
|
||||
category = node.constructor.category?.substring(12);
|
||||
}
|
||||
if(category) {
|
||||
let purename = node.comfyClass.substring(category.length+1);
|
||||
return purename;
|
||||
}
|
||||
else if(node.comfyClass.startsWith('workflow/')) {
|
||||
return node.comfyClass.substring(9);
|
||||
}
|
||||
else {
|
||||
return node.comfyClass;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidVersionString(version) {
|
||||
const versionPattern = /^(\d+)\.(\d+)(\.(\d+))?$/;
|
||||
|
||||
const match = version.match(versionPattern);
|
||||
|
||||
return match !== null &&
|
||||
parseInt(match[1], 10) >= 0 &&
|
||||
parseInt(match[2], 10) >= 0 &&
|
||||
(!match[3] || parseInt(match[4], 10) >= 0);
|
||||
}
|
||||
|
||||
function register_pack_map(name, data) {
|
||||
if(data.packname) {
|
||||
pack_map[data.packname] = name;
|
||||
rpack_map[name] = data;
|
||||
}
|
||||
else {
|
||||
rpack_map[name] = data;
|
||||
}
|
||||
}
|
||||
|
||||
function storeGroupNode(name, data, register=true) {
|
||||
let extra = app.graph.extra;
|
||||
if (!extra) app.graph.extra = extra = {};
|
||||
let groupNodes = extra.groupNodes;
|
||||
if (!groupNodes) extra.groupNodes = groupNodes = {};
|
||||
groupNodes[name] = data;
|
||||
|
||||
if(register) {
|
||||
register_pack_map(name, data);
|
||||
}
|
||||
}
|
||||
|
||||
export async function load_components() {
|
||||
let data = await api.fetchApi('/manager/component/loads', {method: "POST"});
|
||||
let components = await data.json();
|
||||
|
||||
let start_time = Date.now();
|
||||
let failed = [];
|
||||
let failed2 = [];
|
||||
|
||||
for(let name in components) {
|
||||
if(app.graph.extra?.groupNodes?.[name]) {
|
||||
if(data) {
|
||||
let data = components[name];
|
||||
|
||||
let category = data.packname;
|
||||
if(data.category) {
|
||||
category += "/" + data.category;
|
||||
}
|
||||
if(category == '') {
|
||||
category = 'components';
|
||||
}
|
||||
|
||||
const config = new GroupNodeConfig(name, data);
|
||||
await config.registerType(category);
|
||||
|
||||
register_pack_map(name, data);
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let nodeData = components[name];
|
||||
|
||||
storeGroupNode(name, nodeData);
|
||||
|
||||
const config = new GroupNodeConfig(name, nodeData);
|
||||
|
||||
while(true) {
|
||||
try {
|
||||
let category = nodeData.packname;
|
||||
if(nodeData.category) {
|
||||
category += "/" + nodeData.category;
|
||||
}
|
||||
if(category == '') {
|
||||
category = 'components';
|
||||
}
|
||||
|
||||
await config.registerType(category);
|
||||
register_pack_map(name, nodeData);
|
||||
break;
|
||||
}
|
||||
catch {
|
||||
let elapsed_time = Date.now() - start_time;
|
||||
if (elapsed_time > 5000) {
|
||||
failed.push(name);
|
||||
break;
|
||||
} else {
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallback1
|
||||
for(let i in failed) {
|
||||
let name = failed[i];
|
||||
|
||||
if(app.graph.extra?.groupNodes?.[name]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let nodeData = components[name];
|
||||
|
||||
storeGroupNode(name, nodeData);
|
||||
|
||||
const config = new GroupNodeConfig(name, nodeData);
|
||||
while(true) {
|
||||
try {
|
||||
let category = nodeData.packname;
|
||||
if(nodeData.workflow.category) {
|
||||
category += "/" + nodeData.category;
|
||||
}
|
||||
if(category == '') {
|
||||
category = 'components';
|
||||
}
|
||||
|
||||
await config.registerType(category);
|
||||
register_pack_map(name, nodeData);
|
||||
break;
|
||||
}
|
||||
catch {
|
||||
let elapsed_time = Date.now() - start_time;
|
||||
if (elapsed_time > 10000) {
|
||||
failed2.push(name);
|
||||
break;
|
||||
} else {
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallback2
|
||||
for(let name in failed2) {
|
||||
let name = failed2[i];
|
||||
|
||||
let nodeData = components[name];
|
||||
|
||||
storeGroupNode(name, nodeData);
|
||||
|
||||
const config = new GroupNodeConfig(name, nodeData);
|
||||
while(true) {
|
||||
try {
|
||||
let category = nodeData.workflow.packname;
|
||||
if(nodeData.workflow.category) {
|
||||
category += "/" + nodeData.category;
|
||||
}
|
||||
if(category == '') {
|
||||
category = 'components';
|
||||
}
|
||||
|
||||
await config.registerType(category);
|
||||
register_pack_map(name, nodeData);
|
||||
break;
|
||||
}
|
||||
catch {
|
||||
let elapsed_time = Date.now() - start_time;
|
||||
if (elapsed_time > 30000) {
|
||||
failed.push(name);
|
||||
break;
|
||||
} else {
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function save_as_component(node, version, prefix, nodename, packname, category) {
|
||||
let component_name = `${prefix}::${nodename}`;
|
||||
|
||||
let subgraph = app.graph.extra?.groupNodes?.[component_name];
|
||||
if(!subgraph) {
|
||||
subgraph = app.graph.extra?.groupNodes?.[getPureName(node)];
|
||||
}
|
||||
|
||||
subgraph.version = version;
|
||||
subgraph.datetime = Date.now();
|
||||
subgraph.packname = packname;
|
||||
subgraph.category = category;
|
||||
|
||||
let body =
|
||||
{
|
||||
name: component_name,
|
||||
workflow: subgraph
|
||||
};
|
||||
|
||||
pack_map[packname] = component_name;
|
||||
rpack_map[component_name] = subgraph;
|
||||
|
||||
const res = await api.fetchApi('/manager/component/save', {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if(res.status == 200) {
|
||||
storeGroupNode(component_name, subgraph);
|
||||
const config = new GroupNodeConfig(component_name, subgraph);
|
||||
|
||||
let category = body.workflow.packname;
|
||||
if(body.workflow.category) {
|
||||
category += "/" + body.workflow.category;
|
||||
}
|
||||
if(category == '') {
|
||||
category = 'components';
|
||||
}
|
||||
|
||||
await config.registerType(category);
|
||||
|
||||
let path = await res.text();
|
||||
show_message(`Component '${component_name}' is saved into:\n${path}`);
|
||||
}
|
||||
else
|
||||
show_message(`Failed to save component.`);
|
||||
}
|
||||
|
||||
async function import_component(component_name, component, mode) {
|
||||
if(mode) {
|
||||
let body =
|
||||
{
|
||||
name: component_name,
|
||||
workflow: component
|
||||
};
|
||||
|
||||
const res = await api.fetchApi('/manager/component/save', {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
}
|
||||
|
||||
let category = component.packname;
|
||||
if(component.category) {
|
||||
category += "/" + component.category;
|
||||
}
|
||||
if(category == '') {
|
||||
category = 'components';
|
||||
}
|
||||
|
||||
storeGroupNode(component_name, component);
|
||||
const config = new GroupNodeConfig(component_name, component);
|
||||
await config.registerType(category);
|
||||
}
|
||||
|
||||
function restore_to_loaded_component(component_name) {
|
||||
if(rpack_map[component_name]) {
|
||||
let component = rpack_map[component_name];
|
||||
storeGroupNode(component_name, component, false);
|
||||
const config = new GroupNodeConfig(component_name, component);
|
||||
config.registerType(component.category);
|
||||
}
|
||||
}
|
||||
|
||||
// Using a timestamp prevents duplicate pastes and ensures the prevention of re-deletion of litegrapheditor_clipboard.
|
||||
let last_paste_timestamp = null;
|
||||
|
||||
function versionCompare(v1, v2) {
|
||||
let ver1;
|
||||
let ver2;
|
||||
if(v1 && v1 != '') {
|
||||
ver1 = v1.split('.');
|
||||
ver1[0] = parseInt(ver1[0]);
|
||||
ver1[1] = parseInt(ver1[1]);
|
||||
if(ver1.length == 2)
|
||||
ver1.push(0);
|
||||
else
|
||||
ver1[2] = parseInt(ver2[2]);
|
||||
}
|
||||
else {
|
||||
ver1 = [0,0,0];
|
||||
}
|
||||
|
||||
if(v2 && v2 != '') {
|
||||
ver2 = v2.split('.');
|
||||
ver2[0] = parseInt(ver2[0]);
|
||||
ver2[1] = parseInt(ver2[1]);
|
||||
if(ver2.length == 2)
|
||||
ver2.push(0);
|
||||
else
|
||||
ver2[2] = parseInt(ver2[2]);
|
||||
}
|
||||
else {
|
||||
ver2 = [0,0,0];
|
||||
}
|
||||
|
||||
if(ver1[0] > ver2[0])
|
||||
return -1;
|
||||
else if(ver1[0] < ver2[0])
|
||||
return 1;
|
||||
|
||||
if(ver1[1] > ver2[1])
|
||||
return -1;
|
||||
else if(ver1[1] < ver2[1])
|
||||
return 1;
|
||||
|
||||
if(ver1[2] > ver2[2])
|
||||
return -1;
|
||||
else if(ver1[2] < ver2[2])
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function checkVersion(name, component) {
|
||||
let msg = '';
|
||||
if(rpack_map[name]) {
|
||||
let old_version = rpack_map[name].version;
|
||||
if(!old_version || old_version == '') {
|
||||
msg = ` '${name}' Upgrade (V0.0 -> V${component.version})`;
|
||||
}
|
||||
else {
|
||||
let c = versionCompare(old_version, component.version);
|
||||
if(c < 0) {
|
||||
msg = ` '${name}' Downgrade (V${old_version} -> V${component.version})`;
|
||||
}
|
||||
else if(c > 0) {
|
||||
msg = ` '${name}' Upgrade (V${old_version} -> V${component.version})`;
|
||||
}
|
||||
else {
|
||||
msg = ` '${name}' Same version (V${component.version})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
msg = `'${name}' NEW (V${component.version})`;
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
function handle_import_components(components) {
|
||||
let msg = 'Components:\n';
|
||||
let cnt = 0;
|
||||
for(let name in components) {
|
||||
let component = components[name];
|
||||
let v = checkVersion(name, component);
|
||||
|
||||
if(cnt < 10) {
|
||||
msg += v + '\n';
|
||||
}
|
||||
else if (cnt == 10) {
|
||||
msg += '...\n';
|
||||
}
|
||||
else {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
cnt++;
|
||||
}
|
||||
|
||||
let last_name = null;
|
||||
msg += '\nWill you load components?\n';
|
||||
if(confirm(msg)) {
|
||||
let mode = confirm('\nWill you save components?\n(cancel=load without save)');
|
||||
|
||||
for(let name in components) {
|
||||
let component = components[name];
|
||||
import_component(name, component, mode);
|
||||
last_name = name;
|
||||
}
|
||||
|
||||
if(mode) {
|
||||
show_message('Components are saved.');
|
||||
}
|
||||
else {
|
||||
show_message('Components are loaded.');
|
||||
}
|
||||
}
|
||||
|
||||
if(cnt == 1 && last_name) {
|
||||
const node = LiteGraph.createNode(`workflow/${last_name}`);
|
||||
node.pos = [app.canvas.graph_mouse[0], app.canvas.graph_mouse[1]];
|
||||
app.canvas.graph.add(node, false);
|
||||
}
|
||||
}
|
||||
|
||||
function handlePaste(e) {
|
||||
let data = (e.clipboardData || window.clipboardData);
|
||||
const items = data.items;
|
||||
for(const item of items) {
|
||||
if(item.kind == 'string' && item.type == 'text/plain') {
|
||||
data = data.getData("text/plain");
|
||||
try {
|
||||
let json_data = JSON.parse(data);
|
||||
if(json_data.kind == 'ComfyUI Components' && last_paste_timestamp != json_data.timestamp) {
|
||||
last_paste_timestamp = json_data.timestamp;
|
||||
handle_import_components(json_data.components);
|
||||
|
||||
// disable paste node
|
||||
localStorage.removeItem("litegrapheditor_clipboard", null);
|
||||
}
|
||||
else {
|
||||
console.log('This components are already pasted: ignored');
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// nothing to do
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("paste", handlePaste);
|
||||
|
||||
|
||||
export class ComponentBuilderDialog extends ComfyDialog {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
clear() {
|
||||
while (this.element.children.length) {
|
||||
this.element.removeChild(this.element.children[0]);
|
||||
}
|
||||
}
|
||||
|
||||
show() {
|
||||
this.invalidateControl();
|
||||
|
||||
this.element.style.display = "block";
|
||||
this.element.style.zIndex = 10001;
|
||||
this.element.style.width = "500px";
|
||||
this.element.style.height = "450px";
|
||||
}
|
||||
|
||||
invalidateControl() {
|
||||
this.clear();
|
||||
|
||||
let self = this;
|
||||
|
||||
const close_button = $el("button", { id: "cm-close-button", type: "button", textContent: "Close", onclick: () => self.close() });
|
||||
this.save_button = $el("button",
|
||||
{ id: "cm-save-button", type: "button", textContent: "Save", onclick: () =>
|
||||
{
|
||||
save_as_component(self.target_node, self.version_string.value.trim(), self.node_prefix.value.trim(),
|
||||
self.getNodeName(), self.getPackName(), self.category.value.trim());
|
||||
}
|
||||
});
|
||||
|
||||
let default_nodename = getPureName(this.target_node).trim();
|
||||
|
||||
let groupNode = app.graph.extra.groupNodes[default_nodename];
|
||||
let default_packname = groupNode.packname;
|
||||
|
||||
if(!default_packname) {
|
||||
default_packname = '';
|
||||
}
|
||||
|
||||
let default_category = groupNode.category;
|
||||
if(!default_category) {
|
||||
default_category = '';
|
||||
}
|
||||
|
||||
this.default_ver = groupNode.version;
|
||||
if(!this.default_ver) {
|
||||
this.default_ver = '0.0';
|
||||
}
|
||||
|
||||
let delimiterIndex = default_nodename.indexOf('::');
|
||||
let default_prefix = "";
|
||||
if(delimiterIndex != -1) {
|
||||
default_prefix = default_nodename.substring(0, delimiterIndex);
|
||||
default_nodename = default_nodename.substring(delimiterIndex + 2);
|
||||
}
|
||||
|
||||
if(!default_prefix) {
|
||||
this.save_button.disabled = true;
|
||||
}
|
||||
|
||||
this.pack_list = this.createPackListCombo();
|
||||
|
||||
let version_string = this.createLabeledInput('input version (e.g. 1.0)', '*Version : ', this.default_ver);
|
||||
this.version_string = version_string[1];
|
||||
this.version_string.disabled = true;
|
||||
|
||||
let node_prefix = this.createLabeledInput('input node prefix (e.g. mypack)', '*Prefix : ', default_prefix);
|
||||
this.node_prefix = node_prefix[1];
|
||||
|
||||
let manual_nodename = this.createLabeledInput('input node name (e.g. MAKE_BASIC_PIPE)', 'Nodename : ', default_nodename);
|
||||
this.manual_nodename = manual_nodename[1];
|
||||
|
||||
let manual_packname = this.createLabeledInput('input pack name (e.g. mypack)', 'Packname : ', default_packname);
|
||||
this.manual_packname = manual_packname[1];
|
||||
|
||||
let category = this.createLabeledInput('input category (e.g. util/pipe)', 'Category : ', default_category);
|
||||
this.category = category[1];
|
||||
|
||||
this.node_label = this.createNodeLabel();
|
||||
|
||||
let author_mode = this.createAuthorModeCheck();
|
||||
this.author_mode = author_mode[0];
|
||||
|
||||
const content =
|
||||
$el("div.comfy-modal-content",
|
||||
[
|
||||
$el("tr.cm-title", {}, [
|
||||
$el("font", {size:6, color:"white"}, [`ComfyUI-Manager: Component Builder`])]
|
||||
),
|
||||
$el("br", {}, []),
|
||||
$el("div.cm-menu-container",
|
||||
[
|
||||
author_mode[0],
|
||||
author_mode[1],
|
||||
category[0],
|
||||
node_prefix[0],
|
||||
manual_nodename[0],
|
||||
manual_packname[0],
|
||||
version_string[0],
|
||||
this.pack_list,
|
||||
$el("br", {}, []),
|
||||
this.node_label
|
||||
]),
|
||||
|
||||
$el("br", {}, []),
|
||||
this.save_button,
|
||||
close_button,
|
||||
]
|
||||
);
|
||||
|
||||
content.style.width = '100%';
|
||||
content.style.height = '100%';
|
||||
|
||||
this.element = $el("div.comfy-modal", { id:'cm-manager-dialog', parent: document.body }, [ content ]);
|
||||
}
|
||||
|
||||
validateInput() {
|
||||
let msg = "";
|
||||
|
||||
if(!isValidVersionString(this.version_string.value)) {
|
||||
msg += 'Invalid version string: '+event.value+"\n";
|
||||
}
|
||||
|
||||
if(this.node_prefix.value.trim() == '') {
|
||||
msg += 'Node prefix cannot be empty\n';
|
||||
}
|
||||
|
||||
if(this.manual_nodename.value.trim() == '') {
|
||||
msg += 'Node name cannot be empty\n';
|
||||
}
|
||||
|
||||
if(msg != '') {
|
||||
// alert(msg);
|
||||
}
|
||||
|
||||
this.save_button.disabled = msg != "";
|
||||
}
|
||||
|
||||
getPackName() {
|
||||
if(this.pack_list.selectedIndex == 0) {
|
||||
return this.manual_packname.value.trim();
|
||||
}
|
||||
|
||||
return this.pack_list.value.trim();
|
||||
}
|
||||
|
||||
getNodeName() {
|
||||
if(this.manual_nodename.value.trim() != '') {
|
||||
return this.manual_nodename.value.trim();
|
||||
}
|
||||
|
||||
return getPureName(this.target_node);
|
||||
}
|
||||
|
||||
createAuthorModeCheck() {
|
||||
let check = $el("input",{type:'checkbox', id:"author-mode"},[])
|
||||
const check_label = $el("label",{for:"author-mode"},["Enable author mode"]);
|
||||
check_label.style.color = "var(--fg-color)";
|
||||
check_label.style.cursor = "pointer";
|
||||
check.checked = false;
|
||||
|
||||
let self = this;
|
||||
check.onchange = () => {
|
||||
self.version_string.disabled = !check.checked;
|
||||
|
||||
if(!check.checked) {
|
||||
self.version_string.value = self.default_ver;
|
||||
}
|
||||
else {
|
||||
alert('If you are not the author, it is not recommended to change the version, as it may cause component update issues.');
|
||||
}
|
||||
};
|
||||
|
||||
return [check, check_label];
|
||||
}
|
||||
|
||||
createNodeLabel() {
|
||||
let label = $el('p');
|
||||
label.className = 'cb-node-label';
|
||||
if(this.target_node.comfyClass.includes('::'))
|
||||
label.textContent = getPureName(this.target_node);
|
||||
else
|
||||
label.textContent = " _::" + getPureName(this.target_node);
|
||||
return label;
|
||||
}
|
||||
|
||||
createLabeledInput(placeholder, label, value) {
|
||||
let textbox = $el('input.cb-widget-input', {type:'text', placeholder:placeholder, value:value}, []);
|
||||
|
||||
let self = this;
|
||||
textbox.onchange = () => {
|
||||
this.validateInput.call(self);
|
||||
this.node_label.textContent = this.node_prefix.value + "::" + this.manual_nodename.value;
|
||||
}
|
||||
let row = $el('span.cb-widget', {}, [ $el('span.cb-widget-input-label', label), textbox]);
|
||||
|
||||
return [row, textbox];
|
||||
}
|
||||
|
||||
createPackListCombo() {
|
||||
let combo = document.createElement("select");
|
||||
combo.className = "cb-widget";
|
||||
let default_packname_option = { value: '##manual', text: 'Packname: Manual' };
|
||||
|
||||
combo.appendChild($el('option', default_packname_option, []));
|
||||
for(let name in pack_map) {
|
||||
combo.appendChild($el('option', { value: name, text: 'Packname: '+ name }, []));
|
||||
}
|
||||
|
||||
let self = this;
|
||||
combo.onchange = function () {
|
||||
if(combo.selectedIndex == 0) {
|
||||
self.manual_packname.disabled = false;
|
||||
}
|
||||
else {
|
||||
self.manual_packname.disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
return combo;
|
||||
}
|
||||
}
|
||||
|
||||
let orig_handleFile = app.handleFile;
|
||||
|
||||
function handleFile(file) {
|
||||
if (file.name?.endsWith(".json") || file.name?.endsWith(".pack")) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
let is_component = false;
|
||||
const jsonContent = JSON.parse(reader.result);
|
||||
for(let name in jsonContent) {
|
||||
let cand = jsonContent[name];
|
||||
is_component = cand.datetime && cand.version;
|
||||
break;
|
||||
}
|
||||
|
||||
if(is_component) {
|
||||
handle_import_components(jsonContent);
|
||||
}
|
||||
else {
|
||||
orig_handleFile.call(app, file);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
orig_handleFile.call(app, file);
|
||||
}
|
||||
|
||||
app.handleFile = handleFile;
|
||||
|
||||
let current_component_policy = 'workflow';
|
||||
try {
|
||||
api.fetchApi('/manager/component/policy')
|
||||
.then(response => response.text())
|
||||
.then(data => { current_component_policy = data; });
|
||||
}
|
||||
catch {}
|
||||
|
||||
function getChangedVersion(groupNodes) {
|
||||
if(!Object.keys(pack_map).length || !groupNodes)
|
||||
return null;
|
||||
|
||||
let res = {};
|
||||
for(let component_name in groupNodes) {
|
||||
let data = groupNodes[component_name];
|
||||
|
||||
if(rpack_map[component_name]) {
|
||||
let v = versionCompare(data.version, rpack_map[component_name].version);
|
||||
res[component_name] = v;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
const loadGraphData = app.loadGraphData;
|
||||
app.loadGraphData = async function () {
|
||||
if(arguments.length == 0)
|
||||
return await loadGraphData.apply(this, arguments);
|
||||
|
||||
let graphData = arguments[0];
|
||||
let groupNodes = graphData.extra?.groupNodes;
|
||||
let res = getChangedVersion(groupNodes);
|
||||
|
||||
if(res) {
|
||||
let target_components = null;
|
||||
switch(current_component_policy) {
|
||||
case 'higher':
|
||||
target_components = Object.keys(res).filter(key => res[key] == 1);
|
||||
break;
|
||||
|
||||
case 'mine':
|
||||
target_components = Object.keys(res);
|
||||
break;
|
||||
|
||||
default:
|
||||
// do nothing
|
||||
}
|
||||
|
||||
if(target_components) {
|
||||
for(let i in target_components) {
|
||||
let component_name = target_components[i];
|
||||
let component = rpack_map[component_name];
|
||||
if(component && graphData.extra?.groupNodes) {
|
||||
graphData.extra.groupNodes[component_name] = component;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log('Empty components: policy ignored');
|
||||
}
|
||||
|
||||
arguments[0] = graphData;
|
||||
return await loadGraphData.apply(this, arguments);
|
||||
};
|
||||
|
||||
export function set_component_policy(v) {
|
||||
current_component_policy = v;
|
||||
}
|
||||
|
||||
let graphToPrompt = app.graphToPrompt;
|
||||
app.graphToPrompt = async function () {
|
||||
let p = await graphToPrompt.call(app);
|
||||
try {
|
||||
let groupNodes = p.workflow.extra?.groupNodes;
|
||||
if(groupNodes) {
|
||||
p.workflow.extra = { ... p.workflow.extra};
|
||||
|
||||
// get used group nodes
|
||||
let used_group_nodes = new Set();
|
||||
for(let node of p.workflow.nodes) {
|
||||
if(node.type.startsWith('workflow/')) {
|
||||
used_group_nodes.add(node.type.substring(9));
|
||||
}
|
||||
}
|
||||
|
||||
// remove unused group nodes
|
||||
let new_groupNodes = {};
|
||||
for (let key in p.workflow.extra.groupNodes) {
|
||||
if (used_group_nodes.has(key)) {
|
||||
new_groupNodes[key] = p.workflow.extra.groupNodes[key];
|
||||
}
|
||||
}
|
||||
p.workflow.extra.groupNodes = new_groupNodes;
|
||||
}
|
||||
}
|
||||
catch(e) {
|
||||
console.log(`Failed to filtering group nodes: ${e}`);
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { manager_instance, rebootAPI } from "./common.js";
|
||||
|
||||
async function restore_snapshot(target) {
|
||||
if(SnapshotManager.instance) {
|
||||
try {
|
||||
try {
|
||||
const response = await api.fetchApi(`/snapshot/restore?target=${target}`, { cache: "no-store" });
|
||||
if(response.status == 400) {
|
||||
app.ui.dialog.show(`Restore snapshot failed: ${target.title} / ${exception}`);
|
||||
@@ -30,7 +30,7 @@ async function restore_snapshot(target) {
|
||||
|
||||
async function remove_snapshot(target) {
|
||||
if(SnapshotManager.instance) {
|
||||
try {
|
||||
try {
|
||||
const response = await api.fetchApi(`/snapshot/remove?target=${target}`, { cache: "no-store" });
|
||||
if(response.status == 400) {
|
||||
app.ui.dialog.show(`Remove snapshot failed: ${target.title} / ${exception}`);
|
||||
@@ -52,20 +52,20 @@ async function remove_snapshot(target) {
|
||||
}
|
||||
|
||||
async function save_current_snapshot() {
|
||||
try {
|
||||
const response = await api.fetchApi('/snapshot/save', { cache: "no-store" });
|
||||
app.ui.dialog.close();
|
||||
return true;
|
||||
}
|
||||
catch(exception) {
|
||||
app.ui.dialog.show(`Backup snapshot failed: ${exception}`);
|
||||
app.ui.dialog.element.style.zIndex = 10010;
|
||||
return false;
|
||||
}
|
||||
finally {
|
||||
await SnapshotManager.instance.invalidateControl();
|
||||
SnapshotManager.instance.updateMessage("<BR>Current snapshot saved.");
|
||||
}
|
||||
try {
|
||||
const response = await api.fetchApi('/snapshot/save', { cache: "no-store" });
|
||||
app.ui.dialog.close();
|
||||
return true;
|
||||
}
|
||||
catch(exception) {
|
||||
app.ui.dialog.show(`Backup snapshot failed: ${exception}`);
|
||||
app.ui.dialog.element.style.zIndex = 10010;
|
||||
return false;
|
||||
}
|
||||
finally {
|
||||
await SnapshotManager.instance.invalidateControl();
|
||||
SnapshotManager.instance.updateMessage("<BR>Current snapshot saved.");
|
||||
}
|
||||
}
|
||||
|
||||
async function getSnapshotList() {
|
||||
@@ -97,7 +97,7 @@ export class SnapshotManager extends ComfyDialog {
|
||||
async remove_item() {
|
||||
caller.disableButtons();
|
||||
|
||||
await caller.invalidateControl();
|
||||
await caller.invalidateControl();
|
||||
}
|
||||
|
||||
createControls() {
|
||||
@@ -151,14 +151,14 @@ export class SnapshotManager extends ComfyDialog {
|
||||
var grid = document.createElement('table');
|
||||
grid.setAttribute('id', 'snapshot-list-grid');
|
||||
|
||||
var thead = document.createElement('thead');
|
||||
var tbody = document.createElement('tbody');
|
||||
var thead = document.createElement('thead');
|
||||
var tbody = document.createElement('tbody');
|
||||
|
||||
var headerRow = document.createElement('tr');
|
||||
thead.style.position = "sticky";
|
||||
thead.style.top = "0px";
|
||||
thead.style.borderCollapse = "collapse";
|
||||
thead.style.tableLayout = "fixed";
|
||||
thead.style.borderCollapse = "collapse";
|
||||
thead.style.tableLayout = "fixed";
|
||||
|
||||
var header1 = document.createElement('th');
|
||||
header1.innerHTML = ' ID ';
|
||||
@@ -170,7 +170,7 @@ export class SnapshotManager extends ComfyDialog {
|
||||
header_button.innerHTML = 'Action';
|
||||
header_button.style.width = "100px";
|
||||
|
||||
thead.appendChild(headerRow);
|
||||
thead.appendChild(headerRow);
|
||||
headerRow.appendChild(header1);
|
||||
headerRow.appendChild(header2);
|
||||
headerRow.appendChild(header_button);
|
||||
@@ -231,17 +231,17 @@ export class SnapshotManager extends ComfyDialog {
|
||||
this.grid_rows[i] = {data:data, control:dataRow};
|
||||
}
|
||||
|
||||
let self = this;
|
||||
let self = this;
|
||||
const panel = document.createElement('div');
|
||||
panel.style.width = "100%";
|
||||
panel.style.width = "100%";
|
||||
panel.appendChild(grid);
|
||||
|
||||
function handleResize() {
|
||||
const parentHeight = self.element.clientHeight;
|
||||
const gridHeight = parentHeight - 200;
|
||||
function handleResize() {
|
||||
const parentHeight = self.element.clientHeight;
|
||||
const gridHeight = parentHeight - 200;
|
||||
|
||||
grid.style.height = gridHeight + "px";
|
||||
}
|
||||
grid.style.height = gridHeight + "px";
|
||||
}
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
grid.style.position = "relative";
|
||||
@@ -253,7 +253,7 @@ export class SnapshotManager extends ComfyDialog {
|
||||
this.element.style.width = "80%";
|
||||
this.element.appendChild(panel);
|
||||
|
||||
handleResize();
|
||||
handleResize();
|
||||
}
|
||||
|
||||
async createBottomControls() {
|
||||
|
||||
Reference in New Issue
Block a user