mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 10:59:53 +00:00
Apply new code format standard (#217)
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "es5"
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
@@ -1,310 +1,310 @@
|
||||
import type { Page, Locator } from "@playwright/test";
|
||||
import { test as base } from "@playwright/test";
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import type { Page, Locator } from '@playwright/test'
|
||||
import { test as base } from '@playwright/test'
|
||||
import dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface Size {
|
||||
width: number;
|
||||
height: number;
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
class ComfyNodeSearchBox {
|
||||
public readonly input: Locator;
|
||||
public readonly dropdown: Locator;
|
||||
public readonly input: Locator
|
||||
public readonly dropdown: Locator
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.input = page.locator(
|
||||
'.comfy-vue-node-search-container input[type="text"]'
|
||||
);
|
||||
)
|
||||
this.dropdown = page.locator(
|
||||
".comfy-vue-node-search-container .p-autocomplete-list"
|
||||
);
|
||||
'.comfy-vue-node-search-container .p-autocomplete-list'
|
||||
)
|
||||
}
|
||||
|
||||
async fillAndSelectFirstNode(nodeName: string) {
|
||||
await this.input.waitFor({ state: "visible" });
|
||||
await this.input.fill(nodeName);
|
||||
await this.dropdown.waitFor({ state: "visible" });
|
||||
await this.input.waitFor({ state: 'visible' })
|
||||
await this.input.fill(nodeName)
|
||||
await this.dropdown.waitFor({ state: 'visible' })
|
||||
// Wait for some time for the auto complete list to update.
|
||||
// The auto complete list is debounced and may take some time to update.
|
||||
await this.page.waitForTimeout(500);
|
||||
await this.dropdown.locator("li").nth(0).click();
|
||||
await this.page.waitForTimeout(500)
|
||||
await this.dropdown.locator('li').nth(0).click()
|
||||
}
|
||||
}
|
||||
|
||||
class ComfyMenu {
|
||||
public readonly themeToggleButton: Locator;
|
||||
public readonly themeToggleButton: Locator
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.themeToggleButton = page.locator(".comfy-vue-theme-toggle");
|
||||
this.themeToggleButton = page.locator('.comfy-vue-theme-toggle')
|
||||
}
|
||||
|
||||
async toggleTheme() {
|
||||
await this.themeToggleButton.click();
|
||||
await this.themeToggleButton.click()
|
||||
await this.page.evaluate(() => {
|
||||
return new Promise((resolve) => {
|
||||
window["app"].ui.settings.addEventListener(
|
||||
"Comfy.ColorPalette.change",
|
||||
window['app'].ui.settings.addEventListener(
|
||||
'Comfy.ColorPalette.change',
|
||||
resolve,
|
||||
{ once: true }
|
||||
);
|
||||
)
|
||||
|
||||
setTimeout(resolve, 5000);
|
||||
});
|
||||
});
|
||||
setTimeout(resolve, 5000)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async getThemeId() {
|
||||
return await this.page.evaluate(async () => {
|
||||
return await window["app"].ui.settings.getSettingValue(
|
||||
"Comfy.ColorPalette"
|
||||
);
|
||||
});
|
||||
return await window['app'].ui.settings.getSettingValue(
|
||||
'Comfy.ColorPalette'
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class ComfyPage {
|
||||
public readonly url: string;
|
||||
public readonly url: string
|
||||
// All canvas position operations are based on default view of canvas.
|
||||
public readonly canvas: Locator;
|
||||
public readonly widgetTextBox: Locator;
|
||||
public readonly canvas: Locator
|
||||
public readonly widgetTextBox: Locator
|
||||
|
||||
// Buttons
|
||||
public readonly resetViewButton: Locator;
|
||||
public readonly resetViewButton: Locator
|
||||
|
||||
// Inputs
|
||||
public readonly workflowUploadInput: Locator;
|
||||
public readonly workflowUploadInput: Locator
|
||||
|
||||
// Components
|
||||
public readonly searchBox: ComfyNodeSearchBox;
|
||||
public readonly menu: ComfyMenu;
|
||||
public readonly searchBox: ComfyNodeSearchBox
|
||||
public readonly menu: ComfyMenu
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.url = process.env.PLAYWRIGHT_TEST_URL || "http://localhost:8188";
|
||||
this.canvas = page.locator("#graph-canvas");
|
||||
this.widgetTextBox = page.getByPlaceholder("text").nth(1);
|
||||
this.resetViewButton = page.getByRole("button", { name: "Reset View" });
|
||||
this.workflowUploadInput = page.locator("#comfy-file-input");
|
||||
this.searchBox = new ComfyNodeSearchBox(page);
|
||||
this.menu = new ComfyMenu(page);
|
||||
this.url = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'
|
||||
this.canvas = page.locator('#graph-canvas')
|
||||
this.widgetTextBox = page.getByPlaceholder('text').nth(1)
|
||||
this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
|
||||
this.workflowUploadInput = page.locator('#comfy-file-input')
|
||||
this.searchBox = new ComfyNodeSearchBox(page)
|
||||
this.menu = new ComfyMenu(page)
|
||||
}
|
||||
|
||||
async setup() {
|
||||
await this.goto();
|
||||
await this.goto()
|
||||
// Unify font for consistent screenshots.
|
||||
await this.page.addStyleTag({
|
||||
url: "https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap",
|
||||
});
|
||||
url: 'https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap'
|
||||
})
|
||||
await this.page.addStyleTag({
|
||||
url: "https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap",
|
||||
});
|
||||
url: 'https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap'
|
||||
})
|
||||
await this.page.addStyleTag({
|
||||
content: `
|
||||
* {
|
||||
font-family: 'Roboto Mono', 'Noto Color Emoji';
|
||||
}`,
|
||||
});
|
||||
await this.page.waitForFunction(() => document.fonts.ready);
|
||||
}`
|
||||
})
|
||||
await this.page.waitForFunction(() => document.fonts.ready)
|
||||
await this.page.waitForFunction(
|
||||
() => window["app"] !== undefined && window["app"].vueAppReady
|
||||
);
|
||||
() => window['app'] !== undefined && window['app'].vueAppReady
|
||||
)
|
||||
await this.page.evaluate(() => {
|
||||
window["app"]["canvas"].show_info = false;
|
||||
});
|
||||
await this.nextFrame();
|
||||
window['app']['canvas'].show_info = false
|
||||
})
|
||||
await this.nextFrame()
|
||||
// Reset view to force re-rendering of canvas. So that info fields like fps
|
||||
// become hidden.
|
||||
await this.resetView();
|
||||
await this.resetView()
|
||||
}
|
||||
|
||||
async realod() {
|
||||
await this.page.reload({ timeout: 15000 });
|
||||
await this.setup();
|
||||
await this.page.reload({ timeout: 15000 })
|
||||
await this.setup()
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto(this.url);
|
||||
await this.page.goto(this.url)
|
||||
}
|
||||
|
||||
async nextFrame() {
|
||||
await this.page.evaluate(() => {
|
||||
return new Promise<number>(requestAnimationFrame);
|
||||
});
|
||||
return new Promise<number>(requestAnimationFrame)
|
||||
})
|
||||
}
|
||||
|
||||
async loadWorkflow(workflowName: string) {
|
||||
await this.workflowUploadInput.setInputFiles(
|
||||
`./browser_tests/assets/${workflowName}.json`
|
||||
);
|
||||
await this.nextFrame();
|
||||
)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async resetView() {
|
||||
if (await this.resetViewButton.isVisible()) {
|
||||
await this.resetViewButton.click();
|
||||
await this.resetViewButton.click()
|
||||
}
|
||||
// Avoid "Reset View" button highlight.
|
||||
await this.page.mouse.move(10, 10);
|
||||
await this.nextFrame();
|
||||
await this.page.mouse.move(10, 10)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async clickTextEncodeNode1() {
|
||||
await this.canvas.click({
|
||||
position: {
|
||||
x: 618,
|
||||
y: 191,
|
||||
},
|
||||
});
|
||||
await this.nextFrame();
|
||||
y: 191
|
||||
}
|
||||
})
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async clickTextEncodeNode2() {
|
||||
await this.canvas.click({
|
||||
position: {
|
||||
x: 622,
|
||||
y: 400,
|
||||
},
|
||||
});
|
||||
await this.nextFrame();
|
||||
y: 400
|
||||
}
|
||||
})
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async clickEmptySpace() {
|
||||
await this.canvas.click({
|
||||
position: {
|
||||
x: 35,
|
||||
y: 31,
|
||||
},
|
||||
});
|
||||
await this.nextFrame();
|
||||
y: 31
|
||||
}
|
||||
})
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async dragAndDrop(source: Position, target: Position) {
|
||||
await this.page.mouse.move(source.x, source.y);
|
||||
await this.page.mouse.down();
|
||||
await this.page.mouse.move(target.x, target.y);
|
||||
await this.page.mouse.up();
|
||||
await this.nextFrame();
|
||||
await this.page.mouse.move(source.x, source.y)
|
||||
await this.page.mouse.down()
|
||||
await this.page.mouse.move(target.x, target.y)
|
||||
await this.page.mouse.up()
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async dragNode2() {
|
||||
await this.dragAndDrop({ x: 622, y: 400 }, { x: 622, y: 300 });
|
||||
await this.nextFrame();
|
||||
await this.dragAndDrop({ x: 622, y: 400 }, { x: 622, y: 300 })
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async disconnectEdge() {
|
||||
// CLIP input anchor
|
||||
await this.page.mouse.move(427, 198);
|
||||
await this.page.mouse.down();
|
||||
await this.page.mouse.move(427, 98);
|
||||
await this.page.mouse.up();
|
||||
await this.page.mouse.move(427, 198)
|
||||
await this.page.mouse.down()
|
||||
await this.page.mouse.move(427, 98)
|
||||
await this.page.mouse.up()
|
||||
// Move out the way to avoid highlight of menu item.
|
||||
await this.page.mouse.move(10, 10);
|
||||
await this.nextFrame();
|
||||
await this.page.mouse.move(10, 10)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async connectEdge() {
|
||||
// CLIP output anchor on Load Checkpoint Node.
|
||||
await this.page.mouse.move(332, 509);
|
||||
await this.page.mouse.down();
|
||||
await this.page.mouse.move(332, 509)
|
||||
await this.page.mouse.down()
|
||||
// CLIP input anchor on CLIP Text Encode Node.
|
||||
await this.page.mouse.move(427, 198);
|
||||
await this.page.mouse.up();
|
||||
await this.nextFrame();
|
||||
await this.page.mouse.move(427, 198)
|
||||
await this.page.mouse.up()
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async adjustWidgetValue() {
|
||||
// Adjust Empty Latent Image's width input.
|
||||
const page = this.page;
|
||||
await page.locator("#graph-canvas").click({
|
||||
const page = this.page
|
||||
await page.locator('#graph-canvas').click({
|
||||
position: {
|
||||
x: 724,
|
||||
y: 645,
|
||||
},
|
||||
});
|
||||
await page.locator('input[type="text"]').click();
|
||||
await page.locator('input[type="text"]').fill("128");
|
||||
await page.locator('input[type="text"]').press("Enter");
|
||||
await this.nextFrame();
|
||||
y: 645
|
||||
}
|
||||
})
|
||||
await page.locator('input[type="text"]').click()
|
||||
await page.locator('input[type="text"]').fill('128')
|
||||
await page.locator('input[type="text"]').press('Enter')
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async zoom(deltaY: number) {
|
||||
await this.page.mouse.move(10, 10);
|
||||
await this.page.mouse.wheel(0, deltaY);
|
||||
await this.nextFrame();
|
||||
await this.page.mouse.move(10, 10)
|
||||
await this.page.mouse.wheel(0, deltaY)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async pan(offset: Position) {
|
||||
await this.page.mouse.move(10, 10);
|
||||
await this.page.mouse.down();
|
||||
await this.page.mouse.move(offset.x, offset.y);
|
||||
await this.page.mouse.up();
|
||||
await this.nextFrame();
|
||||
await this.page.mouse.move(10, 10)
|
||||
await this.page.mouse.down()
|
||||
await this.page.mouse.move(offset.x, offset.y)
|
||||
await this.page.mouse.up()
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async rightClickCanvas() {
|
||||
await this.page.mouse.click(10, 10, { button: "right" });
|
||||
await this.nextFrame();
|
||||
await this.page.mouse.click(10, 10, { button: 'right' })
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async doubleClickCanvas() {
|
||||
await this.page.mouse.dblclick(10, 10);
|
||||
await this.nextFrame();
|
||||
await this.page.mouse.dblclick(10, 10)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async clickEmptyLatentNode() {
|
||||
await this.canvas.click({
|
||||
position: {
|
||||
x: 724,
|
||||
y: 625,
|
||||
},
|
||||
});
|
||||
this.page.mouse.move(10, 10);
|
||||
await this.nextFrame();
|
||||
y: 625
|
||||
}
|
||||
})
|
||||
this.page.mouse.move(10, 10)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async rightClickEmptyLatentNode() {
|
||||
await this.canvas.click({
|
||||
position: {
|
||||
x: 724,
|
||||
y: 645,
|
||||
y: 645
|
||||
},
|
||||
button: "right",
|
||||
});
|
||||
this.page.mouse.move(10, 10);
|
||||
await this.nextFrame();
|
||||
button: 'right'
|
||||
})
|
||||
this.page.mouse.move(10, 10)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async select2Nodes() {
|
||||
// Select 2 CLIP nodes.
|
||||
await this.page.keyboard.down("Control");
|
||||
await this.clickTextEncodeNode1();
|
||||
await this.clickTextEncodeNode2();
|
||||
await this.page.keyboard.up("Control");
|
||||
await this.nextFrame();
|
||||
await this.page.keyboard.down('Control')
|
||||
await this.clickTextEncodeNode1()
|
||||
await this.clickTextEncodeNode2()
|
||||
await this.page.keyboard.up('Control')
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async ctrlC() {
|
||||
await this.page.keyboard.down("Control");
|
||||
await this.page.keyboard.press("KeyC");
|
||||
await this.page.keyboard.up("Control");
|
||||
await this.nextFrame();
|
||||
await this.page.keyboard.down('Control')
|
||||
await this.page.keyboard.press('KeyC')
|
||||
await this.page.keyboard.up('Control')
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async ctrlV() {
|
||||
await this.page.keyboard.down("Control");
|
||||
await this.page.keyboard.press("KeyV");
|
||||
await this.page.keyboard.up("Control");
|
||||
await this.nextFrame();
|
||||
await this.page.keyboard.down('Control')
|
||||
await this.page.keyboard.press('KeyV')
|
||||
await this.page.keyboard.up('Control')
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async closeMenu() {
|
||||
await this.page.click("button.comfy-close-menu-btn");
|
||||
await this.nextFrame();
|
||||
await this.page.click('button.comfy-close-menu-btn')
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async resizeNode(
|
||||
@@ -316,17 +316,17 @@ export class ComfyPage {
|
||||
) {
|
||||
const bottomRight = {
|
||||
x: nodePos.x + nodeSize.width,
|
||||
y: nodePos.y + nodeSize.height,
|
||||
};
|
||||
y: nodePos.y + nodeSize.height
|
||||
}
|
||||
const target = {
|
||||
x: nodePos.x + nodeSize.width * ratioX,
|
||||
y: nodePos.y + nodeSize.height * ratioY,
|
||||
};
|
||||
await this.dragAndDrop(bottomRight, target);
|
||||
await this.nextFrame();
|
||||
y: nodePos.y + nodeSize.height * ratioY
|
||||
}
|
||||
await this.dragAndDrop(bottomRight, target)
|
||||
await this.nextFrame()
|
||||
if (revertAfter) {
|
||||
await this.dragAndDrop(target, bottomRight);
|
||||
await this.nextFrame();
|
||||
await this.dragAndDrop(target, bottomRight)
|
||||
await this.nextFrame()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,13 +337,13 @@ export class ComfyPage {
|
||||
) {
|
||||
const ksamplerPos = {
|
||||
x: 864,
|
||||
y: 157,
|
||||
};
|
||||
y: 157
|
||||
}
|
||||
const ksamplerSize = {
|
||||
width: 315,
|
||||
height: 292,
|
||||
};
|
||||
this.resizeNode(ksamplerPos, ksamplerSize, percentX, percentY, revertAfter);
|
||||
height: 292
|
||||
}
|
||||
this.resizeNode(ksamplerPos, ksamplerSize, percentX, percentY, revertAfter)
|
||||
}
|
||||
|
||||
async resizeLoadCheckpointNode(
|
||||
@@ -353,19 +353,19 @@ export class ComfyPage {
|
||||
) {
|
||||
const loadCheckpointPos = {
|
||||
x: 25,
|
||||
y: 440,
|
||||
};
|
||||
y: 440
|
||||
}
|
||||
const loadCheckpointSize = {
|
||||
width: 320,
|
||||
height: 120,
|
||||
};
|
||||
height: 120
|
||||
}
|
||||
this.resizeNode(
|
||||
loadCheckpointPos,
|
||||
loadCheckpointSize,
|
||||
percentX,
|
||||
percentY,
|
||||
revertAfter
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
async resizeEmptyLatentNode(
|
||||
@@ -375,26 +375,26 @@ export class ComfyPage {
|
||||
) {
|
||||
const emptyLatentPos = {
|
||||
x: 475,
|
||||
y: 580,
|
||||
};
|
||||
y: 580
|
||||
}
|
||||
const emptyLatentSize = {
|
||||
width: 303,
|
||||
height: 132,
|
||||
};
|
||||
height: 132
|
||||
}
|
||||
this.resizeNode(
|
||||
emptyLatentPos,
|
||||
emptyLatentSize,
|
||||
percentX,
|
||||
percentY,
|
||||
revertAfter
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
|
||||
comfyPage: async ({ page }, use) => {
|
||||
const comfyPage = new ComfyPage(page);
|
||||
await comfyPage.setup();
|
||||
await use(comfyPage);
|
||||
},
|
||||
});
|
||||
const comfyPage = new ComfyPage(page)
|
||||
await comfyPage.setup()
|
||||
await use(comfyPage)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { comfyPageFixture as test } from "./ComfyPage";
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe("Copy Paste", () => {
|
||||
test("Can copy and paste node", async ({ comfyPage }) => {
|
||||
await comfyPage.clickEmptyLatentNode();
|
||||
await comfyPage.page.mouse.move(10, 10);
|
||||
await comfyPage.ctrlC();
|
||||
await comfyPage.ctrlV();
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("copied-node.png");
|
||||
});
|
||||
test.describe('Copy Paste', () => {
|
||||
test('Can copy and paste node', async ({ comfyPage }) => {
|
||||
await comfyPage.clickEmptyLatentNode()
|
||||
await comfyPage.page.mouse.move(10, 10)
|
||||
await comfyPage.ctrlC()
|
||||
await comfyPage.ctrlV()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('copied-node.png')
|
||||
})
|
||||
|
||||
test("Can copy and paste text", async ({ comfyPage }) => {
|
||||
const textBox = comfyPage.widgetTextBox;
|
||||
await textBox.click();
|
||||
const originalString = await textBox.inputValue();
|
||||
await textBox.selectText();
|
||||
await comfyPage.ctrlC();
|
||||
await comfyPage.ctrlV();
|
||||
await comfyPage.ctrlV();
|
||||
const resultString = await textBox.inputValue();
|
||||
expect(resultString).toBe(originalString + originalString);
|
||||
});
|
||||
test('Can copy and paste text', async ({ comfyPage }) => {
|
||||
const textBox = comfyPage.widgetTextBox
|
||||
await textBox.click()
|
||||
const originalString = await textBox.inputValue()
|
||||
await textBox.selectText()
|
||||
await comfyPage.ctrlC()
|
||||
await comfyPage.ctrlV()
|
||||
await comfyPage.ctrlV()
|
||||
const resultString = await textBox.inputValue()
|
||||
expect(resultString).toBe(originalString + originalString)
|
||||
})
|
||||
|
||||
/**
|
||||
* https://github.com/Comfy-Org/ComfyUI_frontend/issues/98
|
||||
*/
|
||||
test("Paste in text area with node previously copied", async ({
|
||||
comfyPage,
|
||||
test('Paste in text area with node previously copied', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.clickEmptyLatentNode();
|
||||
await comfyPage.ctrlC();
|
||||
const textBox = comfyPage.widgetTextBox;
|
||||
await textBox.click();
|
||||
await textBox.inputValue();
|
||||
await textBox.selectText();
|
||||
await comfyPage.ctrlC();
|
||||
await comfyPage.ctrlV();
|
||||
await comfyPage.ctrlV();
|
||||
await comfyPage.clickEmptyLatentNode()
|
||||
await comfyPage.ctrlC()
|
||||
const textBox = comfyPage.widgetTextBox
|
||||
await textBox.click()
|
||||
await textBox.inputValue()
|
||||
await textBox.selectText()
|
||||
await comfyPage.ctrlC()
|
||||
await comfyPage.ctrlV()
|
||||
await comfyPage.ctrlV()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
"paste-in-text-area-with-node-previously-copied.png"
|
||||
);
|
||||
});
|
||||
'paste-in-text-area-with-node-previously-copied.png'
|
||||
)
|
||||
})
|
||||
|
||||
test("Copy text area does not copy node", async ({ comfyPage }) => {
|
||||
const textBox = comfyPage.widgetTextBox;
|
||||
await textBox.click();
|
||||
await textBox.inputValue();
|
||||
await textBox.selectText();
|
||||
await comfyPage.ctrlC();
|
||||
test('Copy text area does not copy node', async ({ comfyPage }) => {
|
||||
const textBox = comfyPage.widgetTextBox
|
||||
await textBox.click()
|
||||
await textBox.inputValue()
|
||||
await textBox.selectText()
|
||||
await comfyPage.ctrlC()
|
||||
// Unfocus textbox.
|
||||
await comfyPage.page.mouse.click(10, 10);
|
||||
await comfyPage.ctrlV();
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("no-node-copied.png");
|
||||
});
|
||||
});
|
||||
await comfyPage.page.mouse.click(10, 10)
|
||||
await comfyPage.ctrlV()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('no-node-copied.png')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,77 +1,75 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { ComfyPage, comfyPageFixture as test } from "./ComfyPage";
|
||||
import { expect } from '@playwright/test'
|
||||
import { ComfyPage, comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe("Node Interaction", () => {
|
||||
test("Can enter prompt", async ({ comfyPage }) => {
|
||||
const textBox = comfyPage.widgetTextBox;
|
||||
await textBox.click();
|
||||
await textBox.fill("Hello World");
|
||||
await expect(textBox).toHaveValue("Hello World");
|
||||
await textBox.fill("Hello World 2");
|
||||
await expect(textBox).toHaveValue("Hello World 2");
|
||||
});
|
||||
test.describe('Node Interaction', () => {
|
||||
test('Can enter prompt', async ({ comfyPage }) => {
|
||||
const textBox = comfyPage.widgetTextBox
|
||||
await textBox.click()
|
||||
await textBox.fill('Hello World')
|
||||
await expect(textBox).toHaveValue('Hello World')
|
||||
await textBox.fill('Hello World 2')
|
||||
await expect(textBox).toHaveValue('Hello World 2')
|
||||
})
|
||||
|
||||
test("Can highlight selected", async ({ comfyPage }) => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("default.png");
|
||||
await comfyPage.clickTextEncodeNode1();
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("selected-node1.png");
|
||||
await comfyPage.clickTextEncodeNode2();
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("selected-node2.png");
|
||||
});
|
||||
test('Can highlight selected', async ({ comfyPage }) => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
|
||||
await comfyPage.clickTextEncodeNode1()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png')
|
||||
await comfyPage.clickTextEncodeNode2()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png')
|
||||
})
|
||||
|
||||
// Flaky. See https://github.com/comfyanonymous/ComfyUI/issues/3866
|
||||
test.skip("Can drag node", async ({ comfyPage }) => {
|
||||
await comfyPage.dragNode2();
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("dragged-node1.png");
|
||||
});
|
||||
test.skip('Can drag node', async ({ comfyPage }) => {
|
||||
await comfyPage.dragNode2()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('dragged-node1.png')
|
||||
})
|
||||
|
||||
test("Can disconnect/connect edge", async ({ comfyPage }) => {
|
||||
await comfyPage.disconnectEdge();
|
||||
test('Can disconnect/connect edge', async ({ comfyPage }) => {
|
||||
await comfyPage.disconnectEdge()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
"disconnected-edge-with-menu.png"
|
||||
);
|
||||
await comfyPage.connectEdge();
|
||||
'disconnected-edge-with-menu.png'
|
||||
)
|
||||
await comfyPage.connectEdge()
|
||||
// Litegraph renders edge with a slight offset.
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("default.png", {
|
||||
maxDiffPixels: 50,
|
||||
});
|
||||
});
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('default.png', {
|
||||
maxDiffPixels: 50
|
||||
})
|
||||
})
|
||||
|
||||
test("Can adjust widget value", async ({ comfyPage }) => {
|
||||
await comfyPage.adjustWidgetValue();
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
"adjusted-widget-value.png"
|
||||
);
|
||||
});
|
||||
test('Can adjust widget value', async ({ comfyPage }) => {
|
||||
await comfyPage.adjustWidgetValue()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('adjusted-widget-value.png')
|
||||
})
|
||||
|
||||
test("Link snap to slot", async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow("snap_to_slot");
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("snap_to_slot.png");
|
||||
test('Link snap to slot', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('snap_to_slot')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot.png')
|
||||
|
||||
const outputSlotPos = {
|
||||
x: 406,
|
||||
y: 333,
|
||||
};
|
||||
y: 333
|
||||
}
|
||||
const samplerNodeCenterPos = {
|
||||
x: 748,
|
||||
y: 77,
|
||||
};
|
||||
await comfyPage.dragAndDrop(outputSlotPos, samplerNodeCenterPos);
|
||||
y: 77
|
||||
}
|
||||
await comfyPage.dragAndDrop(outputSlotPos, samplerNodeCenterPos)
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("snap_to_slot_linked.png");
|
||||
});
|
||||
});
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot_linked.png')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe("Canvas Interaction", () => {
|
||||
test("Can zoom in/out", async ({ comfyPage }) => {
|
||||
await comfyPage.zoom(-100);
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("zoomed-in.png");
|
||||
await comfyPage.zoom(200);
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("zoomed-out.png");
|
||||
});
|
||||
test.describe('Canvas Interaction', () => {
|
||||
test('Can zoom in/out', async ({ comfyPage }) => {
|
||||
await comfyPage.zoom(-100)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in.png')
|
||||
await comfyPage.zoom(200)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-out.png')
|
||||
})
|
||||
|
||||
test("Can pan", async ({ comfyPage }) => {
|
||||
await comfyPage.pan({ x: 200, y: 200 });
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("panned.png");
|
||||
});
|
||||
});
|
||||
test('Can pan', async ({ comfyPage }) => {
|
||||
await comfyPage.pan({ x: 200, y: 200 })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('panned.png')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { comfyPageFixture as test } from "./ComfyPage";
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
function listenForEvent(): Promise<Event> {
|
||||
return new Promise<Event>((resolve) => {
|
||||
document.addEventListener("litegraph:canvas", (e) => resolve(e), {
|
||||
once: true,
|
||||
});
|
||||
});
|
||||
document.addEventListener('litegraph:canvas', (e) => resolve(e), {
|
||||
once: true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
test.describe("Canvas Event", () => {
|
||||
test("Emit litegraph:canvas empty-release", async ({ comfyPage }) => {
|
||||
const eventPromise = comfyPage.page.evaluate(listenForEvent);
|
||||
const disconnectPromise = comfyPage.disconnectEdge();
|
||||
const event = await eventPromise;
|
||||
await disconnectPromise;
|
||||
test.describe('Canvas Event', () => {
|
||||
test('Emit litegraph:canvas empty-release', async ({ comfyPage }) => {
|
||||
const eventPromise = comfyPage.page.evaluate(listenForEvent)
|
||||
const disconnectPromise = comfyPage.disconnectEdge()
|
||||
const event = await eventPromise
|
||||
await disconnectPromise
|
||||
|
||||
expect(event).not.toBeNull();
|
||||
expect(event).not.toBeNull()
|
||||
// No further check on event content as the content is dropped by
|
||||
// playwright for some reason.
|
||||
// See https://github.com/microsoft/playwright/issues/31580
|
||||
});
|
||||
})
|
||||
|
||||
test("Emit litegraph:canvas empty-double-click", async ({ comfyPage }) => {
|
||||
const eventPromise = comfyPage.page.evaluate(listenForEvent);
|
||||
const doubleClickPromise = comfyPage.doubleClickCanvas();
|
||||
const event = await eventPromise;
|
||||
await doubleClickPromise;
|
||||
test('Emit litegraph:canvas empty-double-click', async ({ comfyPage }) => {
|
||||
const eventPromise = comfyPage.page.evaluate(listenForEvent)
|
||||
const doubleClickPromise = comfyPage.doubleClickCanvas()
|
||||
const event = await eventPromise
|
||||
await doubleClickPromise
|
||||
|
||||
expect(event).not.toBeNull();
|
||||
expect(event).not.toBeNull()
|
||||
// No further check on event content as the content is dropped by
|
||||
// playwright for some reason.
|
||||
// See https://github.com/microsoft/playwright/issues/31580
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { comfyPageFixture as test } from "./ComfyPage";
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe("Menu", () => {
|
||||
test.describe('Menu', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(async () => {
|
||||
await window["app"].ui.settings.setSettingValueAsync(
|
||||
"Comfy.UseNewMenu",
|
||||
"Top"
|
||||
);
|
||||
});
|
||||
});
|
||||
await window['app'].ui.settings.setSettingValueAsync(
|
||||
'Comfy.UseNewMenu',
|
||||
'Top'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
const currentThemeId = await comfyPage.menu.getThemeId();
|
||||
if (currentThemeId !== "dark") {
|
||||
await comfyPage.menu.toggleTheme();
|
||||
const currentThemeId = await comfyPage.menu.getThemeId()
|
||||
if (currentThemeId !== 'dark') {
|
||||
await comfyPage.menu.toggleTheme()
|
||||
}
|
||||
await comfyPage.page.evaluate(async () => {
|
||||
await window["app"].ui.settings.setSettingValueAsync(
|
||||
"Comfy.UseNewMenu",
|
||||
"Disabled"
|
||||
);
|
||||
});
|
||||
});
|
||||
await window['app'].ui.settings.setSettingValueAsync(
|
||||
'Comfy.UseNewMenu',
|
||||
'Disabled'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("Toggle theme", async ({ comfyPage }) => {
|
||||
test.setTimeout(30000);
|
||||
test('Toggle theme', async ({ comfyPage }) => {
|
||||
test.setTimeout(30000)
|
||||
|
||||
expect(await comfyPage.menu.getThemeId()).toBe("dark");
|
||||
expect(await comfyPage.menu.getThemeId()).toBe('dark')
|
||||
|
||||
await comfyPage.menu.toggleTheme();
|
||||
await comfyPage.menu.toggleTheme()
|
||||
|
||||
expect(await comfyPage.menu.getThemeId()).toBe("light");
|
||||
expect(await comfyPage.menu.getThemeId()).toBe('light')
|
||||
|
||||
// Theme id should persist after reload.
|
||||
await comfyPage.page.reload();
|
||||
await comfyPage.setup();
|
||||
expect(await comfyPage.menu.getThemeId()).toBe("light");
|
||||
await comfyPage.page.reload()
|
||||
await comfyPage.setup()
|
||||
expect(await comfyPage.menu.getThemeId()).toBe('light')
|
||||
|
||||
await comfyPage.menu.toggleTheme();
|
||||
await comfyPage.menu.toggleTheme()
|
||||
|
||||
expect(await comfyPage.menu.getThemeId()).toBe("dark");
|
||||
});
|
||||
});
|
||||
expect(await comfyPage.menu.getThemeId()).toBe('dark')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { comfyPageFixture as test } from "./ComfyPage";
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe("Node search box", () => {
|
||||
test("Can trigger on empty canvas double click", async ({ comfyPage }) => {
|
||||
await comfyPage.doubleClickCanvas();
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1);
|
||||
});
|
||||
test.describe('Node search box', () => {
|
||||
test('Can trigger on empty canvas double click', async ({ comfyPage }) => {
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1)
|
||||
})
|
||||
|
||||
test("Can trigger on link release", async ({ comfyPage }) => {
|
||||
await comfyPage.page.keyboard.down("Shift");
|
||||
await comfyPage.disconnectEdge();
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1);
|
||||
});
|
||||
test('Can trigger on link release', async ({ comfyPage }) => {
|
||||
await comfyPage.page.keyboard.down('Shift')
|
||||
await comfyPage.disconnectEdge()
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1)
|
||||
})
|
||||
|
||||
test("Does not trigger on link release (no shift)", async ({ comfyPage }) => {
|
||||
await comfyPage.disconnectEdge();
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(0);
|
||||
});
|
||||
test('Does not trigger on link release (no shift)', async ({ comfyPage }) => {
|
||||
await comfyPage.disconnectEdge()
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(0)
|
||||
})
|
||||
|
||||
test("Can add node", async ({ comfyPage }) => {
|
||||
await comfyPage.doubleClickCanvas();
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1);
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode("KSampler");
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("added-node.png");
|
||||
});
|
||||
test('Can add node', async ({ comfyPage }) => {
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1)
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('added-node.png')
|
||||
})
|
||||
|
||||
test("Can auto link node", async ({ comfyPage }) => {
|
||||
await comfyPage.page.keyboard.down("Shift");
|
||||
await comfyPage.disconnectEdge();
|
||||
await comfyPage.page.keyboard.up("Shift");
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode("CLIPTextEncode");
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("auto-linked-node.png");
|
||||
});
|
||||
});
|
||||
test('Can auto link node', async ({ comfyPage }) => {
|
||||
await comfyPage.page.keyboard.down('Shift')
|
||||
await comfyPage.disconnectEdge()
|
||||
await comfyPage.page.keyboard.up('Shift')
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('CLIPTextEncode')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('auto-linked-node.png')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,92 +1,88 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { comfyPageFixture as test } from "./ComfyPage";
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe("Canvas Right Click Menu", () => {
|
||||
test.describe('Canvas Right Click Menu', () => {
|
||||
// See https://github.com/comfyanonymous/ComfyUI/issues/3883
|
||||
// Right-click menu on canvas's option sequence is not stable.
|
||||
test.skip("Can add node", async ({ comfyPage }) => {
|
||||
await comfyPage.rightClickCanvas();
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("right-click-menu.png");
|
||||
await comfyPage.page.getByText("Add Node").click();
|
||||
await comfyPage.nextFrame();
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("add-node-menu.png");
|
||||
await comfyPage.page.getByText("loaders").click();
|
||||
await comfyPage.nextFrame();
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
"add-node-menu-loaders.png"
|
||||
);
|
||||
await comfyPage.page.getByText("Load VAE").click();
|
||||
await comfyPage.nextFrame();
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("add-node-node-added.png");
|
||||
});
|
||||
test.skip('Can add node', async ({ comfyPage }) => {
|
||||
await comfyPage.rightClickCanvas()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
|
||||
await comfyPage.page.getByText('Add Node').click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('add-node-menu.png')
|
||||
await comfyPage.page.getByText('loaders').click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('add-node-menu-loaders.png')
|
||||
await comfyPage.page.getByText('Load VAE').click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('add-node-node-added.png')
|
||||
})
|
||||
|
||||
// See https://github.com/comfyanonymous/ComfyUI/issues/3883
|
||||
// Right-click menu on canvas's option sequence is not stable.
|
||||
test.skip("Can add group", async ({ comfyPage }) => {
|
||||
await comfyPage.rightClickCanvas();
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("right-click-menu.png");
|
||||
await comfyPage.page.getByText("Add Group", { exact: true }).click();
|
||||
await comfyPage.nextFrame();
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
"add-group-group-added.png"
|
||||
);
|
||||
});
|
||||
test.skip('Can add group', async ({ comfyPage }) => {
|
||||
await comfyPage.rightClickCanvas()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
|
||||
await comfyPage.page.getByText('Add Group', { exact: true }).click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('add-group-group-added.png')
|
||||
})
|
||||
|
||||
test("Can convert to group node", async ({ comfyPage }) => {
|
||||
await comfyPage.select2Nodes();
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("selected-2-nodes.png");
|
||||
comfyPage.page.on("dialog", async (dialog) => {
|
||||
await dialog.accept("GroupNode2CLIP");
|
||||
});
|
||||
await comfyPage.rightClickCanvas();
|
||||
await comfyPage.page.getByText("Convert to Group Node").click();
|
||||
await comfyPage.nextFrame();
|
||||
test('Can convert to group node', async ({ comfyPage }) => {
|
||||
await comfyPage.select2Nodes()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png')
|
||||
comfyPage.page.on('dialog', async (dialog) => {
|
||||
await dialog.accept('GroupNode2CLIP')
|
||||
})
|
||||
await comfyPage.rightClickCanvas()
|
||||
await comfyPage.page.getByText('Convert to Group Node').click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
"right-click-node-group-node.png"
|
||||
);
|
||||
});
|
||||
});
|
||||
'right-click-node-group-node.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe("Node Right Click Menu", () => {
|
||||
test("Can open properties panel", async ({ comfyPage }) => {
|
||||
await comfyPage.rightClickEmptyLatentNode();
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("right-click-node.png");
|
||||
await comfyPage.page.getByText("Properties Panel").click();
|
||||
await comfyPage.nextFrame();
|
||||
test.describe('Node Right Click Menu', () => {
|
||||
test('Can open properties panel', async ({ comfyPage }) => {
|
||||
await comfyPage.rightClickEmptyLatentNode()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
|
||||
await comfyPage.page.getByText('Properties Panel').click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
"right-click-node-properties-panel.png"
|
||||
);
|
||||
});
|
||||
'right-click-node-properties-panel.png'
|
||||
)
|
||||
})
|
||||
|
||||
test("Can collapse", async ({ comfyPage }) => {
|
||||
await comfyPage.rightClickEmptyLatentNode();
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("right-click-node.png");
|
||||
await comfyPage.page.getByText("Collapse").click();
|
||||
await comfyPage.nextFrame();
|
||||
test('Can collapse', async ({ comfyPage }) => {
|
||||
await comfyPage.rightClickEmptyLatentNode()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
|
||||
await comfyPage.page.getByText('Collapse').click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
"right-click-node-collapsed.png"
|
||||
);
|
||||
});
|
||||
'right-click-node-collapsed.png'
|
||||
)
|
||||
})
|
||||
|
||||
test("Can bypass", async ({ comfyPage }) => {
|
||||
await comfyPage.rightClickEmptyLatentNode();
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("right-click-node.png");
|
||||
await comfyPage.page.getByText("Bypass").click();
|
||||
await comfyPage.nextFrame();
|
||||
test('Can bypass', async ({ comfyPage }) => {
|
||||
await comfyPage.rightClickEmptyLatentNode()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
|
||||
await comfyPage.page.getByText('Bypass').click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
"right-click-node-bypassed.png"
|
||||
);
|
||||
});
|
||||
'right-click-node-bypassed.png'
|
||||
)
|
||||
})
|
||||
|
||||
test("Can convert widget to input", async ({ comfyPage }) => {
|
||||
await comfyPage.rightClickEmptyLatentNode();
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("right-click-node.png");
|
||||
await comfyPage.page.getByText("Convert Widget to Input").click();
|
||||
await comfyPage.nextFrame();
|
||||
await comfyPage.page.getByText("Convert width to input").click();
|
||||
await comfyPage.nextFrame();
|
||||
test('Can convert widget to input', async ({ comfyPage }) => {
|
||||
await comfyPage.rightClickEmptyLatentNode()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
|
||||
await comfyPage.page.getByText('Convert Widget to Input').click()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.getByText('Convert width to input').click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
"right-click-node-widget-converted.png"
|
||||
);
|
||||
});
|
||||
});
|
||||
'right-click-node-widget-converted.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { comfyPageFixture as test } from "./ComfyPage";
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe("Combo text widget", () => {
|
||||
test("Truncates text when resized", async ({ comfyPage }) => {
|
||||
await comfyPage.resizeLoadCheckpointNode(0.2, 1);
|
||||
test.describe('Combo text widget', () => {
|
||||
test('Truncates text when resized', async ({ comfyPage }) => {
|
||||
await comfyPage.resizeLoadCheckpointNode(0.2, 1)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
"load-checkpoint-resized-min-width.png"
|
||||
);
|
||||
await comfyPage.closeMenu();
|
||||
await comfyPage.resizeKsamplerNode(0.2, 1);
|
||||
'load-checkpoint-resized-min-width.png'
|
||||
)
|
||||
await comfyPage.closeMenu()
|
||||
await comfyPage.resizeKsamplerNode(0.2, 1)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
`ksampler-resized-min-width.png`
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
test("Doesn't truncate when space still available", async ({ comfyPage }) => {
|
||||
await comfyPage.resizeEmptyLatentNode(0.8, 0.8);
|
||||
await comfyPage.resizeEmptyLatentNode(0.8, 0.8)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
"empty-latent-resized-80-percent.png"
|
||||
);
|
||||
});
|
||||
'empty-latent-resized-80-percent.png'
|
||||
)
|
||||
})
|
||||
|
||||
test("Can revert to full text", async ({ comfyPage }) => {
|
||||
await comfyPage.resizeLoadCheckpointNode(0.8, 1, true);
|
||||
await expect(comfyPage.canvas).toHaveScreenshot("resized-to-original.png");
|
||||
});
|
||||
});
|
||||
test('Can revert to full text', async ({ comfyPage }) => {
|
||||
await comfyPage.resizeLoadCheckpointNode(0.8, 1, true)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('resized-to-original.png')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import type { JestConfigWithTsJest } from "ts-jest";
|
||||
import type { JestConfigWithTsJest } from 'ts-jest'
|
||||
|
||||
const jestConfig: JestConfigWithTsJest = {
|
||||
testMatch: ["**/tests-ui/**/*.test.ts"],
|
||||
testEnvironment: "jsdom",
|
||||
testMatch: ['**/tests-ui/**/*.test.ts'],
|
||||
testEnvironment: 'jsdom',
|
||||
transform: {
|
||||
"^.+\\.m?[tj]sx?$": [
|
||||
"ts-jest",
|
||||
'^.+\\.m?[tj]sx?$': [
|
||||
'ts-jest',
|
||||
{
|
||||
tsconfig: "./tsconfig.json",
|
||||
babelConfig: "./babel.config.json",
|
||||
tsconfig: './tsconfig.json',
|
||||
babelConfig: './babel.config.json'
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
setupFiles: ["./tests-ui/globalSetup.ts"],
|
||||
setupFilesAfterEnv: ["./tests-ui/afterSetup.ts"],
|
||||
setupFiles: ['./tests-ui/globalSetup.ts'],
|
||||
setupFilesAfterEnv: ['./tests-ui/afterSetup.ts'],
|
||||
clearMocks: true,
|
||||
resetModules: true,
|
||||
testTimeout: 10000,
|
||||
moduleNameMapper: {
|
||||
"^@/(.*)$": "<rootDir>/src/$1",
|
||||
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
|
||||
},
|
||||
};
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
'\\.(css|less|scss|sass)$': 'identity-obj-proxy'
|
||||
}
|
||||
}
|
||||
|
||||
export default jestConfig;
|
||||
export default jestConfig
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
@@ -11,7 +11,7 @@ import { defineConfig, devices } from "@playwright/test";
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./browser_tests",
|
||||
testDir: './browser_tests',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
@@ -21,23 +21,23 @@ export default defineConfig({
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "html",
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://127.0.0.1:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
trace: 'on-first-retry'
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
timeout: 5000,
|
||||
},
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
timeout: 5000
|
||||
}
|
||||
|
||||
// {
|
||||
// name: 'firefox',
|
||||
@@ -68,7 +68,7 @@ export default defineConfig({
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
]
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
@@ -76,4 +76,4 @@ export default defineConfig({
|
||||
// url: 'http://127.0.0.1:3000',
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
// },
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { copy } from "fs-extra";
|
||||
import { config } from "dotenv";
|
||||
config();
|
||||
import { copy } from 'fs-extra'
|
||||
import { config } from 'dotenv'
|
||||
config()
|
||||
|
||||
const sourceDir = "./dist";
|
||||
const targetDir = process.env.DEPLOY_COMFYUI_DIR;
|
||||
const sourceDir = './dist'
|
||||
const targetDir = process.env.DEPLOY_COMFYUI_DIR
|
||||
|
||||
copy(sourceDir, targetDir)
|
||||
.then(() => {
|
||||
console.log(`Directory copied successfully! ${sourceDir} -> ${targetDir}`);
|
||||
console.log(`Directory copied successfully! ${sourceDir} -> ${targetDir}`)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error copying directory:", err);
|
||||
});
|
||||
console.error('Error copying directory:', err)
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import zipdir from "zip-dir";
|
||||
import zipdir from 'zip-dir'
|
||||
|
||||
zipdir("./dist", { saveTo: "./dist.zip" }, function (err, buffer) {
|
||||
zipdir('./dist', { saveTo: './dist.zip' }, function (err, buffer) {
|
||||
if (err) {
|
||||
console.error('Error zipping "dist" directory:', err);
|
||||
console.error('Error zipping "dist" directory:', err)
|
||||
} else {
|
||||
console.log('Successfully zipped "dist" directory.');
|
||||
console.log('Successfully zipped "dist" directory.')
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
74
src/App.vue
74
src/App.vue
@@ -13,65 +13,65 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, markRaw, onMounted, ref, watch } from "vue";
|
||||
import NodeSearchboxPopover from "@/components/NodeSearchBoxPopover.vue";
|
||||
import SideToolBar from "@/components/sidebar/SideToolBar.vue";
|
||||
import LiteGraphCanvasSplitterOverlay from "@/components/LiteGraphCanvasSplitterOverlay.vue";
|
||||
import QueueSideBarTab from "@/components/sidebar/tabs/QueueSideBarTab.vue";
|
||||
import ProgressSpinner from "primevue/progressspinner";
|
||||
import { app } from "./scripts/app";
|
||||
import { useSettingStore } from "./stores/settingStore";
|
||||
import { useNodeDefStore } from "./stores/nodeDefStore";
|
||||
import { ExtensionManagerImpl } from "./scripts/extensionManager";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { computed, markRaw, onMounted, ref, watch } from 'vue'
|
||||
import NodeSearchboxPopover from '@/components/NodeSearchBoxPopover.vue'
|
||||
import SideToolBar from '@/components/sidebar/SideToolBar.vue'
|
||||
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
|
||||
import QueueSideBarTab from '@/components/sidebar/tabs/QueueSideBarTab.vue'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { app } from './scripts/app'
|
||||
import { useSettingStore } from './stores/settingStore'
|
||||
import { useNodeDefStore } from './stores/nodeDefStore'
|
||||
import { ExtensionManagerImpl } from './scripts/extensionManager'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const isLoading = ref(true);
|
||||
const isLoading = ref(true)
|
||||
const nodeSearchEnabled = computed<boolean>(
|
||||
() => useSettingStore().get("Comfy.NodeSearchBoxImpl") === "default"
|
||||
);
|
||||
() => useSettingStore().get('Comfy.NodeSearchBoxImpl') === 'default'
|
||||
)
|
||||
const theme = computed<string>(() =>
|
||||
useSettingStore().get("Comfy.ColorPalette")
|
||||
);
|
||||
useSettingStore().get('Comfy.ColorPalette')
|
||||
)
|
||||
watch(
|
||||
theme,
|
||||
(newTheme) => {
|
||||
const DARK_THEME_CLASS = "dark-theme";
|
||||
const isDarkTheme = newTheme !== "light";
|
||||
const DARK_THEME_CLASS = 'dark-theme'
|
||||
const isDarkTheme = newTheme !== 'light'
|
||||
if (isDarkTheme) {
|
||||
document.body.classList.add(DARK_THEME_CLASS);
|
||||
document.body.classList.add(DARK_THEME_CLASS)
|
||||
} else {
|
||||
document.body.classList.remove(DARK_THEME_CLASS);
|
||||
document.body.classList.remove(DARK_THEME_CLASS)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
)
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t } = useI18n()
|
||||
const init = () => {
|
||||
useNodeDefStore().addNodeDefs(Object.values(app.nodeDefs));
|
||||
useSettingStore().addSettings(app.ui.settings);
|
||||
app.vueAppReady = true;
|
||||
useNodeDefStore().addNodeDefs(Object.values(app.nodeDefs))
|
||||
useSettingStore().addSettings(app.ui.settings)
|
||||
app.vueAppReady = true
|
||||
// Late init as extension manager needs to access pinia store.
|
||||
app.extensionManager = new ExtensionManagerImpl();
|
||||
app.extensionManager = new ExtensionManagerImpl()
|
||||
app.extensionManager.registerSidebarTab({
|
||||
id: "queue",
|
||||
icon: "pi pi-history",
|
||||
title: t("sideToolBar.queue"),
|
||||
tooltip: t("sideToolBar.queue"),
|
||||
id: 'queue',
|
||||
icon: 'pi pi-history',
|
||||
title: t('sideToolBar.queue'),
|
||||
tooltip: t('sideToolBar.queue'),
|
||||
component: markRaw(QueueSideBarTab),
|
||||
type: "vue",
|
||||
});
|
||||
};
|
||||
type: 'vue'
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
init();
|
||||
init()
|
||||
} catch (e) {
|
||||
console.error("Failed to init Vue app", e);
|
||||
console.error('Failed to init Vue app', e)
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
isLoading.value = false
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -15,17 +15,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useWorkspaceStore } from "@/stores/workspaceStateStore";
|
||||
import Splitter from "primevue/splitter";
|
||||
import SplitterPanel from "primevue/splitterpanel";
|
||||
import { computed } from "vue";
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
|
||||
import Splitter from 'primevue/splitter'
|
||||
import SplitterPanel from 'primevue/splitterpanel'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const sideBarPanelVisible = computed(
|
||||
() => useWorkspaceStore().activeSidebarTab !== null
|
||||
);
|
||||
)
|
||||
const gutterClass = computed(() => {
|
||||
return sideBarPanelVisible.value ? "" : "gutter-hidden";
|
||||
});
|
||||
return sideBarPanelVisible.value ? '' : 'gutter-hidden'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -19,10 +19,10 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
|
||||
<div class="_sb_col">
|
||||
<div v-if="slotInput" :class="['_sb_dot', slotInput.type]"></div>
|
||||
</div>
|
||||
<div class="_sb_col">{{ slotInput ? slotInput.name : "" }}</div>
|
||||
<div class="_sb_col">{{ slotInput ? slotInput.name : '' }}</div>
|
||||
<div class="_sb_col middle-column"></div>
|
||||
<div class="_sb_col _sb_inherit">
|
||||
{{ slotOutput ? slotOutput.name : "" }}
|
||||
{{ slotOutput ? slotOutput.name : '' }}
|
||||
</div>
|
||||
<div class="_sb_col">
|
||||
<div v-if="slotOutput" :class="['_sb_dot', slotOutput.type]"></div>
|
||||
@@ -45,40 +45,40 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { app } from "@/scripts/app";
|
||||
import { type ComfyNodeDef } from "@/types/apiTypes";
|
||||
import _ from "lodash";
|
||||
import { PropType } from "vue";
|
||||
import { app } from '@/scripts/app'
|
||||
import { type ComfyNodeDef } from '@/types/apiTypes'
|
||||
import _ from 'lodash'
|
||||
import { PropType } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
nodeDef: {
|
||||
type: Object as PropType<ComfyNodeDef>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const nodeDef = props.nodeDef as ComfyNodeDef;
|
||||
const nodeDef = props.nodeDef as ComfyNodeDef
|
||||
|
||||
// --------------------------------------------------
|
||||
// TODO: Move out to separate file
|
||||
interface IComfyNodeInputDef {
|
||||
name: string;
|
||||
type: string;
|
||||
widgetType: string | null;
|
||||
defaultValue: any;
|
||||
name: string
|
||||
type: string
|
||||
widgetType: string | null
|
||||
defaultValue: any
|
||||
}
|
||||
|
||||
interface IComfyNodeOutputDef {
|
||||
name: string | null;
|
||||
type: string;
|
||||
isList: boolean;
|
||||
name: string | null
|
||||
type: string
|
||||
isList: boolean
|
||||
}
|
||||
|
||||
const allInputs = Object.assign(
|
||||
{},
|
||||
nodeDef.input.required || {},
|
||||
nodeDef.input.optional || {}
|
||||
);
|
||||
)
|
||||
const allInputDefs: IComfyNodeInputDef[] = Object.entries(allInputs).map(
|
||||
([inputName, inputData]) => {
|
||||
return {
|
||||
@@ -87,10 +87,10 @@ const allInputDefs: IComfyNodeInputDef[] = Object.entries(allInputs).map(
|
||||
widgetType: app.getWidgetType(inputData, inputName),
|
||||
defaultValue:
|
||||
inputData[1]?.default ||
|
||||
(inputData[0] instanceof Array ? inputData[0][0] : ""),
|
||||
};
|
||||
(inputData[0] instanceof Array ? inputData[0][0] : '')
|
||||
}
|
||||
);
|
||||
}
|
||||
)
|
||||
|
||||
const allOutputDefs: IComfyNodeOutputDef[] = _.zip(
|
||||
nodeDef.output,
|
||||
@@ -99,13 +99,13 @@ const allOutputDefs: IComfyNodeOutputDef[] = _.zip(
|
||||
).map(([outputType, outputName, isList]) => {
|
||||
return {
|
||||
name: outputName,
|
||||
type: outputType instanceof Array ? "COMBO" : outputType,
|
||||
isList: isList,
|
||||
};
|
||||
});
|
||||
type: outputType instanceof Array ? 'COMBO' : outputType,
|
||||
isList: isList
|
||||
}
|
||||
})
|
||||
|
||||
const slotInputDefs = allInputDefs.filter((input) => !input.widgetType);
|
||||
const widgetInputDefs = allInputDefs.filter((input) => !!input.widgetType);
|
||||
const slotInputDefs = allInputDefs.filter((input) => !input.widgetType)
|
||||
const widgetInputDefs = allInputDefs.filter((input) => !!input.widgetType)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -174,7 +174,7 @@ const widgetInputDefs = allInputDefs.filter((input) => !!input.widgetType);
|
||||
|
||||
._sb_node_preview {
|
||||
background-color: var(--comfy-menu-bg);
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: small;
|
||||
color: var(--descrip-text);
|
||||
border: 1px solid var(--descrip-text);
|
||||
|
||||
@@ -53,73 +53,73 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, onMounted, Ref, ref } from "vue";
|
||||
import AutoCompletePlus from "./primevueOverride/AutoCompletePlus.vue";
|
||||
import Chip from "primevue/chip";
|
||||
import Badge from "primevue/badge";
|
||||
import NodeSearchFilter from "@/components/NodeSearchFilter.vue";
|
||||
import NodeSourceChip from "@/components/NodeSourceChip.vue";
|
||||
import { ComfyNodeDef } from "@/types/apiTypes";
|
||||
import { type FilterAndValue } from "@/services/nodeSearchService";
|
||||
import NodePreview from "./NodePreview.vue";
|
||||
import { useNodeDefStore } from "@/stores/nodeDefStore";
|
||||
import { computed, inject, onMounted, Ref, ref } from 'vue'
|
||||
import AutoCompletePlus from './primevueOverride/AutoCompletePlus.vue'
|
||||
import Chip from 'primevue/chip'
|
||||
import Badge from 'primevue/badge'
|
||||
import NodeSearchFilter from '@/components/NodeSearchFilter.vue'
|
||||
import NodeSourceChip from '@/components/NodeSourceChip.vue'
|
||||
import { ComfyNodeDef } from '@/types/apiTypes'
|
||||
import { type FilterAndValue } from '@/services/nodeSearchService'
|
||||
import NodePreview from './NodePreview.vue'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
|
||||
const props = defineProps({
|
||||
filters: {
|
||||
type: Array<FilterAndValue>,
|
||||
type: Array<FilterAndValue>
|
||||
},
|
||||
searchLimit: {
|
||||
type: Number,
|
||||
default: 64,
|
||||
},
|
||||
});
|
||||
default: 64
|
||||
}
|
||||
})
|
||||
|
||||
const inputId = `comfy-vue-node-search-box-input-${Math.random()}`;
|
||||
const suggestions = ref<ComfyNodeDef[]>([]);
|
||||
const hoveredSuggestion = ref<ComfyNodeDef | null>(null);
|
||||
const inputId = `comfy-vue-node-search-box-input-${Math.random()}`
|
||||
const suggestions = ref<ComfyNodeDef[]>([])
|
||||
const hoveredSuggestion = ref<ComfyNodeDef | null>(null)
|
||||
const placeholder = computed(() => {
|
||||
return props.filters.length === 0 ? "Search for nodes" : "";
|
||||
});
|
||||
return props.filters.length === 0 ? 'Search for nodes' : ''
|
||||
})
|
||||
|
||||
const search = (query: string) => {
|
||||
suggestions.value = useNodeDefStore().nodeSearchService.searchNode(
|
||||
query,
|
||||
props.filters,
|
||||
{
|
||||
limit: props.searchLimit,
|
||||
limit: props.searchLimit
|
||||
}
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const emit = defineEmits(["addFilter", "removeFilter", "addNode"]);
|
||||
const emit = defineEmits(['addFilter', 'removeFilter', 'addNode'])
|
||||
|
||||
const reFocusInput = () => {
|
||||
const inputElement = document.getElementById(inputId) as HTMLInputElement;
|
||||
const inputElement = document.getElementById(inputId) as HTMLInputElement
|
||||
if (inputElement) {
|
||||
inputElement.blur();
|
||||
inputElement.focus();
|
||||
inputElement.blur()
|
||||
inputElement.focus()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(reFocusInput);
|
||||
onMounted(reFocusInput)
|
||||
const onAddFilter = (filterAndValue: FilterAndValue) => {
|
||||
emit("addFilter", filterAndValue);
|
||||
reFocusInput();
|
||||
};
|
||||
emit('addFilter', filterAndValue)
|
||||
reFocusInput()
|
||||
}
|
||||
const onRemoveFilter = (event: Event, filterAndValue: FilterAndValue) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
emit("removeFilter", filterAndValue);
|
||||
reFocusInput();
|
||||
};
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
emit('removeFilter', filterAndValue)
|
||||
reFocusInput()
|
||||
}
|
||||
const setHoverSuggestion = (index: number) => {
|
||||
if (index === -1) {
|
||||
hoveredSuggestion.value = null;
|
||||
return;
|
||||
hoveredSuggestion.value = null
|
||||
return
|
||||
}
|
||||
const value = suggestions.value[index];
|
||||
hoveredSuggestion.value = value;
|
||||
};
|
||||
const value = suggestions.value[index]
|
||||
hoveredSuggestion.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -21,142 +21,142 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { app } from "@/scripts/app";
|
||||
import { inject, onMounted, onUnmounted, reactive, Ref, ref } from "vue";
|
||||
import NodeSearchBox from "./NodeSearchBox.vue";
|
||||
import Dialog from "primevue/dialog";
|
||||
import { app } from '@/scripts/app'
|
||||
import { inject, onMounted, onUnmounted, reactive, Ref, ref } from 'vue'
|
||||
import NodeSearchBox from './NodeSearchBox.vue'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import {
|
||||
INodeSlot,
|
||||
LiteGraph,
|
||||
LiteGraphCanvasEvent,
|
||||
LGraphNode,
|
||||
LinkReleaseContext,
|
||||
} from "@comfyorg/litegraph";
|
||||
import { FilterAndValue } from "@/services/nodeSearchService";
|
||||
import { ComfyNodeDef } from "@/types/apiTypes";
|
||||
import { useNodeDefStore } from "@/stores/nodeDefStore";
|
||||
LinkReleaseContext
|
||||
} from '@comfyorg/litegraph'
|
||||
import { FilterAndValue } from '@/services/nodeSearchService'
|
||||
import { ComfyNodeDef } from '@/types/apiTypes'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
|
||||
interface LiteGraphPointerEvent extends Event {
|
||||
canvasX: number;
|
||||
canvasY: number;
|
||||
canvasX: number
|
||||
canvasY: number
|
||||
}
|
||||
|
||||
const visible = ref(false);
|
||||
const dismissable = ref(true);
|
||||
const triggerEvent = ref<LiteGraphCanvasEvent | null>(null);
|
||||
const visible = ref(false)
|
||||
const dismissable = ref(true)
|
||||
const triggerEvent = ref<LiteGraphCanvasEvent | null>(null)
|
||||
const getNewNodeLocation = (): [number, number] => {
|
||||
if (triggerEvent.value === null) {
|
||||
return [100, 100];
|
||||
return [100, 100]
|
||||
}
|
||||
|
||||
const originalEvent = triggerEvent.value.detail
|
||||
.originalEvent as LiteGraphPointerEvent;
|
||||
return [originalEvent.canvasX, originalEvent.canvasY];
|
||||
};
|
||||
const nodeFilters = reactive([]);
|
||||
.originalEvent as LiteGraphPointerEvent
|
||||
return [originalEvent.canvasX, originalEvent.canvasY]
|
||||
}
|
||||
const nodeFilters = reactive([])
|
||||
const addFilter = (filter: FilterAndValue) => {
|
||||
nodeFilters.push(filter);
|
||||
};
|
||||
nodeFilters.push(filter)
|
||||
}
|
||||
const removeFilter = (filter: FilterAndValue) => {
|
||||
const index = nodeFilters.findIndex((f) => f === filter);
|
||||
const index = nodeFilters.findIndex((f) => f === filter)
|
||||
if (index !== -1) {
|
||||
nodeFilters.splice(index, 1);
|
||||
nodeFilters.splice(index, 1)
|
||||
}
|
||||
};
|
||||
}
|
||||
const clearFilters = () => {
|
||||
nodeFilters.splice(0, nodeFilters.length);
|
||||
};
|
||||
nodeFilters.splice(0, nodeFilters.length)
|
||||
}
|
||||
const closeDialog = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
visible.value = false
|
||||
}
|
||||
const connectNodeOnLinkRelease = (
|
||||
node: LGraphNode,
|
||||
context: LinkReleaseContext
|
||||
) => {
|
||||
const destIsInput = context.node_from !== undefined;
|
||||
const destIsInput = context.node_from !== undefined
|
||||
const srcNode = (
|
||||
destIsInput ? context.node_from : context.node_to
|
||||
) as LGraphNode;
|
||||
const srcSlotIndex: number = context.slot_from.slot_index;
|
||||
) as LGraphNode
|
||||
const srcSlotIndex: number = context.slot_from.slot_index
|
||||
const linkDataType = destIsInput
|
||||
? context.type_filter_in
|
||||
: context.type_filter_out;
|
||||
const destSlots = destIsInput ? node.inputs : node.outputs;
|
||||
: context.type_filter_out
|
||||
const destSlots = destIsInput ? node.inputs : node.outputs
|
||||
const destSlotIndex = destSlots.findIndex(
|
||||
(slot: INodeSlot) => slot.type === linkDataType
|
||||
);
|
||||
)
|
||||
|
||||
if (destSlotIndex === -1) {
|
||||
console.warn(
|
||||
`Could not find slot with type ${linkDataType} on node ${node.title}`
|
||||
);
|
||||
return;
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (destIsInput) {
|
||||
srcNode.connect(srcSlotIndex, node, destSlotIndex);
|
||||
srcNode.connect(srcSlotIndex, node, destSlotIndex)
|
||||
} else {
|
||||
node.connect(destSlotIndex, srcNode, srcSlotIndex);
|
||||
node.connect(destSlotIndex, srcNode, srcSlotIndex)
|
||||
}
|
||||
};
|
||||
}
|
||||
const addNode = (nodeDef: ComfyNodeDef) => {
|
||||
closeDialog();
|
||||
const node = LiteGraph.createNode(nodeDef.name, nodeDef.display_name, {});
|
||||
closeDialog()
|
||||
const node = LiteGraph.createNode(nodeDef.name, nodeDef.display_name, {})
|
||||
if (node) {
|
||||
node.pos = getNewNodeLocation();
|
||||
app.graph.add(node);
|
||||
node.pos = getNewNodeLocation()
|
||||
app.graph.add(node)
|
||||
|
||||
const eventDetail = triggerEvent.value.detail;
|
||||
if (eventDetail.subType === "empty-release") {
|
||||
connectNodeOnLinkRelease(node, eventDetail.linkReleaseContext);
|
||||
const eventDetail = triggerEvent.value.detail
|
||||
if (eventDetail.subType === 'empty-release') {
|
||||
connectNodeOnLinkRelease(node, eventDetail.linkReleaseContext)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const canvasEventHandler = (e: LiteGraphCanvasEvent) => {
|
||||
const shiftPressed = (e.detail.originalEvent as KeyboardEvent).shiftKey;
|
||||
const shiftPressed = (e.detail.originalEvent as KeyboardEvent).shiftKey
|
||||
// Ignore empty releases unless shift is pressed
|
||||
// Empty release without shift is trigger right click menu
|
||||
if (e.detail.subType === "empty-release" && !shiftPressed) {
|
||||
return;
|
||||
if (e.detail.subType === 'empty-release' && !shiftPressed) {
|
||||
return
|
||||
}
|
||||
|
||||
if (e.detail.subType === "empty-release") {
|
||||
const destIsInput = e.detail.linkReleaseContext.node_from !== undefined;
|
||||
if (e.detail.subType === 'empty-release') {
|
||||
const destIsInput = e.detail.linkReleaseContext.node_from !== undefined
|
||||
const filter = useNodeDefStore().nodeSearchService.getFilterById(
|
||||
destIsInput ? "input" : "output"
|
||||
);
|
||||
destIsInput ? 'input' : 'output'
|
||||
)
|
||||
|
||||
const value = destIsInput
|
||||
? e.detail.linkReleaseContext.type_filter_in
|
||||
: e.detail.linkReleaseContext.type_filter_out;
|
||||
: e.detail.linkReleaseContext.type_filter_out
|
||||
|
||||
addFilter([filter, value]);
|
||||
addFilter([filter, value])
|
||||
}
|
||||
triggerEvent.value = e;
|
||||
visible.value = true;
|
||||
triggerEvent.value = e
|
||||
visible.value = true
|
||||
// Prevent the dialog from being dismissed immediately
|
||||
dismissable.value = false;
|
||||
dismissable.value = false
|
||||
setTimeout(() => {
|
||||
dismissable.value = true;
|
||||
}, 300);
|
||||
};
|
||||
dismissable.value = true
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const handleEscapeKeyPress = (event) => {
|
||||
if (event.key === "Escape") {
|
||||
closeDialog();
|
||||
if (event.key === 'Escape') {
|
||||
closeDialog()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener("litegraph:canvas", canvasEventHandler);
|
||||
document.addEventListener("keydown", handleEscapeKeyPress);
|
||||
});
|
||||
document.addEventListener('litegraph:canvas', canvasEventHandler)
|
||||
document.addEventListener('keydown', handleEscapeKeyPress)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("litegraph:canvas", canvasEventHandler);
|
||||
document.removeEventListener("keydown", handleEscapeKeyPress);
|
||||
});
|
||||
document.removeEventListener('litegraph:canvas', canvasEventHandler)
|
||||
document.removeEventListener('keydown', handleEscapeKeyPress)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -34,52 +34,52 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NodeFilter, type FilterAndValue } from "@/services/nodeSearchService";
|
||||
import Button from "primevue/button";
|
||||
import Dialog from "primevue/dialog";
|
||||
import SelectButton from "primevue/selectbutton";
|
||||
import AutoComplete from "primevue/autocomplete";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useNodeDefStore } from "@/stores/nodeDefStore";
|
||||
import { NodeFilter, type FilterAndValue } from '@/services/nodeSearchService'
|
||||
import Button from 'primevue/button'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import AutoComplete from 'primevue/autocomplete'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
|
||||
const visible = ref<boolean>(false);
|
||||
const filters = ref<NodeFilter[]>([]);
|
||||
const selectedFilter = ref<NodeFilter>();
|
||||
const filterValues = ref<string[]>([]);
|
||||
const selectedFilterValue = ref<string>("");
|
||||
const visible = ref<boolean>(false)
|
||||
const filters = ref<NodeFilter[]>([])
|
||||
const selectedFilter = ref<NodeFilter>()
|
||||
const filterValues = ref<string[]>([])
|
||||
const selectedFilterValue = ref<string>('')
|
||||
|
||||
onMounted(() => {
|
||||
const nodeSearchService = useNodeDefStore().nodeSearchService;
|
||||
filters.value = nodeSearchService.nodeFilters;
|
||||
selectedFilter.value = nodeSearchService.nodeFilters[0];
|
||||
});
|
||||
const nodeSearchService = useNodeDefStore().nodeSearchService
|
||||
filters.value = nodeSearchService.nodeFilters
|
||||
selectedFilter.value = nodeSearchService.nodeFilters[0]
|
||||
})
|
||||
|
||||
const emit = defineEmits(["addFilter"]);
|
||||
const emit = defineEmits(['addFilter'])
|
||||
|
||||
const updateSelectedFilterValue = () => {
|
||||
updateFilterValues("");
|
||||
updateFilterValues('')
|
||||
if (filterValues.value.includes(selectedFilterValue.value)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
selectedFilterValue.value = filterValues.value[0];
|
||||
};
|
||||
selectedFilterValue.value = filterValues.value[0]
|
||||
}
|
||||
|
||||
const updateFilterValues = (query: string) => {
|
||||
filterValues.value = selectedFilter.value.fuseSearch.search(query);
|
||||
};
|
||||
filterValues.value = selectedFilter.value.fuseSearch.search(query)
|
||||
}
|
||||
|
||||
const submit = () => {
|
||||
visible.value = false;
|
||||
emit("addFilter", [
|
||||
visible.value = false
|
||||
emit('addFilter', [
|
||||
selectedFilter.value,
|
||||
selectedFilterValue.value,
|
||||
] as FilterAndValue);
|
||||
};
|
||||
selectedFilterValue.value
|
||||
] as FilterAndValue)
|
||||
}
|
||||
|
||||
const showModal = () => {
|
||||
updateSelectedFilterValue();
|
||||
visible.value = true;
|
||||
};
|
||||
updateSelectedFilterValue()
|
||||
visible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -5,18 +5,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getNodeSource } from "@/types/nodeSource";
|
||||
import Chip from "primevue/chip";
|
||||
import { computed } from "vue";
|
||||
import { getNodeSource } from '@/types/nodeSource'
|
||||
import Chip from 'primevue/chip'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
python_module: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const nodeSource = computed(() => getNodeSource(props.python_module));
|
||||
const nodeSource = computed(() => getNodeSource(props.python_module))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<!-- Auto complete with extra event "focused-option-changed" -->
|
||||
<script>
|
||||
import AutoComplete from "primevue/autocomplete";
|
||||
import AutoComplete from 'primevue/autocomplete'
|
||||
|
||||
export default {
|
||||
name: "AutoCompletePlus",
|
||||
name: 'AutoCompletePlus',
|
||||
extends: AutoComplete,
|
||||
emits: ["focused-option-changed"],
|
||||
emits: ['focused-option-changed'],
|
||||
mounted() {
|
||||
if (typeof AutoComplete.mounted === "function") {
|
||||
AutoComplete.mounted.call(this);
|
||||
if (typeof AutoComplete.mounted === 'function') {
|
||||
AutoComplete.mounted.call(this)
|
||||
}
|
||||
|
||||
// Add a watcher on the focusedOptionIndex property
|
||||
@@ -16,9 +16,9 @@ export default {
|
||||
() => this.focusedOptionIndex,
|
||||
(newVal, oldVal) => {
|
||||
// Emit a custom event when focusedOptionIndex changes
|
||||
this.$emit("focused-option-changed", newVal);
|
||||
this.$emit('focused-option-changed', newVal)
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
? 'p-button-primary side-bar-button-selected'
|
||||
: 'p-button-secondary'
|
||||
}`,
|
||||
icon: 'side-bar-button-icon',
|
||||
icon: 'side-bar-button-icon'
|
||||
}"
|
||||
@click="emit('click', $event)"
|
||||
v-tooltip="{ value: props.tooltip, showDelay: 300, hideDelay: 300 }"
|
||||
@@ -17,22 +17,22 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from "primevue/button";
|
||||
import Button from 'primevue/button'
|
||||
|
||||
const props = defineProps({
|
||||
icon: String,
|
||||
selected: Boolean,
|
||||
tooltip: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: ''
|
||||
},
|
||||
class: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(["click"]);
|
||||
const emit = defineEmits(['click'])
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { app } from "@/scripts/app";
|
||||
import SideBarIcon from "./SideBarIcon.vue";
|
||||
import { app } from '@/scripts/app'
|
||||
import SideBarIcon from './SideBarIcon.vue'
|
||||
|
||||
const showSetting = () => {
|
||||
app.ui.settings.show();
|
||||
};
|
||||
app.ui.settings.show()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -8,23 +8,23 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import SideBarIcon from "./SideBarIcon.vue";
|
||||
import { useSettingStore } from "@/stores/settingStore";
|
||||
import { computed, ref } from 'vue'
|
||||
import SideBarIcon from './SideBarIcon.vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const previousDarkTheme = ref("dark");
|
||||
const previousDarkTheme = ref('dark')
|
||||
const currentTheme = computed(() =>
|
||||
useSettingStore().get("Comfy.ColorPalette", "dark")
|
||||
);
|
||||
const isDarkMode = computed(() => currentTheme.value !== "light");
|
||||
const icon = computed(() => (isDarkMode.value ? "pi pi-moon" : "pi pi-sun"));
|
||||
useSettingStore().get('Comfy.ColorPalette', 'dark')
|
||||
)
|
||||
const isDarkMode = computed(() => currentTheme.value !== 'light')
|
||||
const icon = computed(() => (isDarkMode.value ? 'pi pi-moon' : 'pi pi-sun'))
|
||||
|
||||
const toggleTheme = () => {
|
||||
if (isDarkMode.value) {
|
||||
previousDarkTheme.value = currentTheme.value;
|
||||
useSettingStore().set("Comfy.ColorPalette", "light");
|
||||
previousDarkTheme.value = currentTheme.value
|
||||
useSettingStore().set('Comfy.ColorPalette', 'light')
|
||||
} else {
|
||||
useSettingStore().set("Comfy.ColorPalette", previousDarkTheme.value);
|
||||
useSettingStore().set('Comfy.ColorPalette', previousDarkTheme.value)
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -28,55 +28,55 @@
|
||||
mountCustomTab(
|
||||
selectedTab as CustomSidebarTabExtension,
|
||||
el as HTMLElement
|
||||
);
|
||||
)
|
||||
}
|
||||
"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SideBarIcon from "./SideBarIcon.vue";
|
||||
import SideBarThemeToggleIcon from "./SideBarThemeToggleIcon.vue";
|
||||
import SideBarSettingsToggleIcon from "./SideBarSettingsToggleIcon.vue";
|
||||
import { computed, onBeforeUnmount, watch } from "vue";
|
||||
import { useSettingStore } from "@/stores/settingStore";
|
||||
import { app } from "@/scripts/app";
|
||||
import { useWorkspaceStore } from "@/stores/workspaceStateStore";
|
||||
import SideBarIcon from './SideBarIcon.vue'
|
||||
import SideBarThemeToggleIcon from './SideBarThemeToggleIcon.vue'
|
||||
import SideBarSettingsToggleIcon from './SideBarSettingsToggleIcon.vue'
|
||||
import { computed, onBeforeUnmount, watch } from 'vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
|
||||
import {
|
||||
CustomSidebarTabExtension,
|
||||
SidebarTabExtension,
|
||||
} from "@/types/extensionTypes";
|
||||
SidebarTabExtension
|
||||
} from '@/types/extensionTypes'
|
||||
|
||||
const workspaceStateStore = useWorkspaceStore();
|
||||
const tabs = computed(() => app.extensionManager.getSidebarTabs());
|
||||
const workspaceStateStore = useWorkspaceStore()
|
||||
const tabs = computed(() => app.extensionManager.getSidebarTabs())
|
||||
const selectedTab = computed<SidebarTabExtension | null>(() => {
|
||||
const tabId = workspaceStateStore.activeSidebarTab;
|
||||
return tabs.value.find((tab) => tab.id === tabId) || null;
|
||||
});
|
||||
const tabId = workspaceStateStore.activeSidebarTab
|
||||
return tabs.value.find((tab) => tab.id === tabId) || null
|
||||
})
|
||||
const mountCustomTab = (tab: CustomSidebarTabExtension, el: HTMLElement) => {
|
||||
tab.render(el);
|
||||
};
|
||||
tab.render(el)
|
||||
}
|
||||
const onTabClick = (item: SidebarTabExtension) => {
|
||||
workspaceStateStore.updateActiveSidebarTab(
|
||||
workspaceStateStore.activeSidebarTab === item.id ? null : item.id
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const betaMenuEnabled = computed(
|
||||
() => useSettingStore().get("Comfy.UseNewMenu") !== "Disabled"
|
||||
);
|
||||
() => useSettingStore().get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
watch(betaMenuEnabled, (newValue) => {
|
||||
if (!newValue) {
|
||||
workspaceStateStore.updateActiveSidebarTab(null);
|
||||
workspaceStateStore.updateActiveSidebarTab(null)
|
||||
}
|
||||
});
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
tabs.value.forEach((tab) => {
|
||||
if (tab.type === "custom" && tab.destroy) {
|
||||
tab.destroy();
|
||||
if (tab.type === 'custom' && tab.destroy) {
|
||||
tab.destroy()
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -26,11 +26,11 @@
|
||||
<Column
|
||||
:pt="{
|
||||
headerCell: {
|
||||
class: 'queue-tool-header-cell',
|
||||
class: 'queue-tool-header-cell'
|
||||
},
|
||||
bodyCell: {
|
||||
class: 'queue-tool-body-cell',
|
||||
},
|
||||
class: 'queue-tool-body-cell'
|
||||
}
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
@@ -67,89 +67,89 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import DataTable from "primevue/datatable";
|
||||
import Column from "primevue/column";
|
||||
import Tag from "primevue/tag";
|
||||
import Button from "primevue/button";
|
||||
import ConfirmPopup from "primevue/confirmpopup";
|
||||
import Toast from "primevue/toast";
|
||||
import Message from "primevue/message";
|
||||
import { useConfirm } from "primevue/useconfirm";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import DataTable from 'primevue/datatable'
|
||||
import Column from 'primevue/column'
|
||||
import Tag from 'primevue/tag'
|
||||
import Button from 'primevue/button'
|
||||
import ConfirmPopup from 'primevue/confirmpopup'
|
||||
import Toast from 'primevue/toast'
|
||||
import Message from 'primevue/message'
|
||||
import { useConfirm } from 'primevue/useconfirm'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import {
|
||||
TaskItemDisplayStatus,
|
||||
TaskItemImpl,
|
||||
useQueueStore,
|
||||
} from "@/stores/queueStore";
|
||||
import { computed, onMounted } from "vue";
|
||||
import { api } from "@/scripts/api";
|
||||
useQueueStore
|
||||
} from '@/stores/queueStore'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
const confirm = useConfirm();
|
||||
const toast = useToast();
|
||||
const queueStore = useQueueStore();
|
||||
const confirm = useConfirm()
|
||||
const toast = useToast()
|
||||
const queueStore = useQueueStore()
|
||||
|
||||
const tasks = computed(() => queueStore.tasks);
|
||||
const tasks = computed(() => queueStore.tasks)
|
||||
const taskTagSeverity = (status: TaskItemDisplayStatus) => {
|
||||
switch (status) {
|
||||
case TaskItemDisplayStatus.Pending:
|
||||
return "secondary";
|
||||
return 'secondary'
|
||||
case TaskItemDisplayStatus.Running:
|
||||
return "info";
|
||||
return 'info'
|
||||
case TaskItemDisplayStatus.Completed:
|
||||
return "success";
|
||||
return 'success'
|
||||
case TaskItemDisplayStatus.Failed:
|
||||
return "danger";
|
||||
return 'danger'
|
||||
case TaskItemDisplayStatus.Cancelled:
|
||||
return "warning";
|
||||
return 'warning'
|
||||
}
|
||||
};
|
||||
}
|
||||
const formatTime = (time?: number) => {
|
||||
if (time === undefined) {
|
||||
return "";
|
||||
return ''
|
||||
}
|
||||
return `${time.toFixed(2)}s`;
|
||||
};
|
||||
return `${time.toFixed(2)}s`
|
||||
}
|
||||
const removeTask = (task: TaskItemImpl) => {
|
||||
if (task.isRunning) {
|
||||
api.interrupt();
|
||||
api.interrupt()
|
||||
}
|
||||
queueStore.delete(task);
|
||||
};
|
||||
queueStore.delete(task)
|
||||
}
|
||||
const removeAllTasks = async () => {
|
||||
await queueStore.clear();
|
||||
};
|
||||
await queueStore.clear()
|
||||
}
|
||||
const confirmRemoveAll = (event) => {
|
||||
confirm.require({
|
||||
target: event.currentTarget,
|
||||
message: "Do you want to delete all tasks?",
|
||||
icon: "pi pi-info-circle",
|
||||
message: 'Do you want to delete all tasks?',
|
||||
icon: 'pi pi-info-circle',
|
||||
rejectProps: {
|
||||
label: "Cancel",
|
||||
severity: "secondary",
|
||||
outlined: true,
|
||||
label: 'Cancel',
|
||||
severity: 'secondary',
|
||||
outlined: true
|
||||
},
|
||||
acceptProps: {
|
||||
label: "Delete",
|
||||
severity: "danger",
|
||||
label: 'Delete',
|
||||
severity: 'danger'
|
||||
},
|
||||
accept: async () => {
|
||||
await removeAllTasks();
|
||||
await removeAllTasks()
|
||||
toast.add({
|
||||
severity: "info",
|
||||
summary: "Confirmed",
|
||||
detail: "Tasks deleted",
|
||||
life: 3000,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
severity: 'info',
|
||||
summary: 'Confirmed',
|
||||
detail: 'Tasks deleted',
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
onMounted(() => {
|
||||
api.addEventListener("status", () => {
|
||||
queueStore.update();
|
||||
});
|
||||
api.addEventListener('status', () => {
|
||||
queueStore.update()
|
||||
})
|
||||
|
||||
queueStore.update();
|
||||
});
|
||||
queueStore.update()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { app } from "../../scripts/app";
|
||||
import { ComfyDialog, $el } from "../../scripts/ui";
|
||||
import { ComfyApp } from "../../scripts/app";
|
||||
import { app } from '../../scripts/app'
|
||||
import { ComfyDialog, $el } from '../../scripts/ui'
|
||||
import { ComfyApp } from '../../scripts/app'
|
||||
|
||||
export class ClipspaceDialog extends ComfyDialog {
|
||||
static items = [];
|
||||
static instance = null;
|
||||
static items = []
|
||||
static instance = null
|
||||
|
||||
static registerButton(name, contextPredicate, callback) {
|
||||
const item = $el("button", {
|
||||
type: "button",
|
||||
const item = $el('button', {
|
||||
type: 'button',
|
||||
textContent: name,
|
||||
contextPredicate: contextPredicate,
|
||||
onclick: callback,
|
||||
});
|
||||
onclick: callback
|
||||
})
|
||||
|
||||
ClipspaceDialog.items.push(item);
|
||||
ClipspaceDialog.items.push(item)
|
||||
}
|
||||
|
||||
static invalidatePreview() {
|
||||
@@ -24,161 +24,161 @@ export class ClipspaceDialog extends ComfyDialog {
|
||||
ComfyApp.clipspace.imgs.length > 0
|
||||
) {
|
||||
const img_preview = document.getElementById(
|
||||
"clipspace_preview"
|
||||
) as HTMLImageElement;
|
||||
'clipspace_preview'
|
||||
) as HTMLImageElement
|
||||
if (img_preview) {
|
||||
img_preview.src =
|
||||
ComfyApp.clipspace.imgs[ComfyApp.clipspace["selectedIndex"]].src;
|
||||
img_preview.style.maxHeight = "100%";
|
||||
img_preview.style.maxWidth = "100%";
|
||||
ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src
|
||||
img_preview.style.maxHeight = '100%'
|
||||
img_preview.style.maxWidth = '100%'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static invalidate() {
|
||||
if (ClipspaceDialog.instance) {
|
||||
const self = ClipspaceDialog.instance;
|
||||
const self = ClipspaceDialog.instance
|
||||
// allow reconstruct controls when copying from non-image to image content.
|
||||
const children = $el("div.comfy-modal-content", [
|
||||
const children = $el('div.comfy-modal-content', [
|
||||
self.createImgSettings(),
|
||||
...self.createButtons(),
|
||||
]);
|
||||
...self.createButtons()
|
||||
])
|
||||
|
||||
if (self.element) {
|
||||
// update
|
||||
self.element.removeChild(self.element.firstChild);
|
||||
self.element.appendChild(children);
|
||||
self.element.removeChild(self.element.firstChild)
|
||||
self.element.appendChild(children)
|
||||
} else {
|
||||
// new
|
||||
self.element = $el("div.comfy-modal", { parent: document.body }, [
|
||||
children,
|
||||
]);
|
||||
self.element = $el('div.comfy-modal', { parent: document.body }, [
|
||||
children
|
||||
])
|
||||
}
|
||||
|
||||
if (self.element.children[0].children.length <= 1) {
|
||||
self.element.children[0].appendChild(
|
||||
$el("p", {}, [
|
||||
"Unable to find the features to edit content of a format stored in the current Clipspace.",
|
||||
$el('p', {}, [
|
||||
'Unable to find the features to edit content of a format stored in the current Clipspace.'
|
||||
])
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
ClipspaceDialog.invalidatePreview();
|
||||
ClipspaceDialog.invalidatePreview()
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
super()
|
||||
}
|
||||
|
||||
createButtons() {
|
||||
const buttons = [];
|
||||
const buttons = []
|
||||
|
||||
for (let idx in ClipspaceDialog.items) {
|
||||
const item = ClipspaceDialog.items[idx];
|
||||
const item = ClipspaceDialog.items[idx]
|
||||
if (!item.contextPredicate || item.contextPredicate())
|
||||
buttons.push(ClipspaceDialog.items[idx]);
|
||||
buttons.push(ClipspaceDialog.items[idx])
|
||||
}
|
||||
|
||||
buttons.push(
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Close",
|
||||
$el('button', {
|
||||
type: 'button',
|
||||
textContent: 'Close',
|
||||
onclick: () => {
|
||||
this.close();
|
||||
},
|
||||
this.close()
|
||||
}
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
return buttons;
|
||||
return buttons
|
||||
}
|
||||
|
||||
createImgSettings() {
|
||||
if (ComfyApp.clipspace.imgs) {
|
||||
const combo_items = [];
|
||||
const imgs = ComfyApp.clipspace.imgs;
|
||||
const combo_items = []
|
||||
const imgs = ComfyApp.clipspace.imgs
|
||||
|
||||
for (let i = 0; i < imgs.length; i++) {
|
||||
combo_items.push($el("option", { value: i }, [`${i}`]));
|
||||
combo_items.push($el('option', { value: i }, [`${i}`]))
|
||||
}
|
||||
|
||||
const combo1 = $el(
|
||||
"select",
|
||||
'select',
|
||||
{
|
||||
id: "clipspace_img_selector",
|
||||
id: 'clipspace_img_selector',
|
||||
onchange: (event) => {
|
||||
ComfyApp.clipspace["selectedIndex"] = event.target.selectedIndex;
|
||||
ClipspaceDialog.invalidatePreview();
|
||||
},
|
||||
ComfyApp.clipspace['selectedIndex'] = event.target.selectedIndex
|
||||
ClipspaceDialog.invalidatePreview()
|
||||
}
|
||||
},
|
||||
combo_items
|
||||
);
|
||||
)
|
||||
|
||||
const row1 = $el("tr", {}, [
|
||||
$el("td", {}, [$el("font", { color: "white" }, ["Select Image"])]),
|
||||
$el("td", {}, [combo1]),
|
||||
]);
|
||||
const row1 = $el('tr', {}, [
|
||||
$el('td', {}, [$el('font', { color: 'white' }, ['Select Image'])]),
|
||||
$el('td', {}, [combo1])
|
||||
])
|
||||
|
||||
const combo2 = $el(
|
||||
"select",
|
||||
'select',
|
||||
{
|
||||
id: "clipspace_img_paste_mode",
|
||||
id: 'clipspace_img_paste_mode',
|
||||
onchange: (event) => {
|
||||
ComfyApp.clipspace["img_paste_mode"] = event.target.value;
|
||||
},
|
||||
ComfyApp.clipspace['img_paste_mode'] = event.target.value
|
||||
}
|
||||
},
|
||||
[
|
||||
$el("option", { value: "selected" }, "selected"),
|
||||
$el("option", { value: "all" }, "all"),
|
||||
$el('option', { value: 'selected' }, 'selected'),
|
||||
$el('option', { value: 'all' }, 'all')
|
||||
]
|
||||
) as HTMLSelectElement;
|
||||
combo2.value = ComfyApp.clipspace["img_paste_mode"];
|
||||
) as HTMLSelectElement
|
||||
combo2.value = ComfyApp.clipspace['img_paste_mode']
|
||||
|
||||
const row2 = $el("tr", {}, [
|
||||
$el("td", {}, [$el("font", { color: "white" }, ["Paste Mode"])]),
|
||||
$el("td", {}, [combo2]),
|
||||
]);
|
||||
const row2 = $el('tr', {}, [
|
||||
$el('td', {}, [$el('font', { color: 'white' }, ['Paste Mode'])]),
|
||||
$el('td', {}, [combo2])
|
||||
])
|
||||
|
||||
const td = $el(
|
||||
"td",
|
||||
{ align: "center", width: "100px", height: "100px", colSpan: "2" },
|
||||
[$el("img", { id: "clipspace_preview", ondragstart: () => false }, [])]
|
||||
);
|
||||
'td',
|
||||
{ align: 'center', width: '100px', height: '100px', colSpan: '2' },
|
||||
[$el('img', { id: 'clipspace_preview', ondragstart: () => false }, [])]
|
||||
)
|
||||
|
||||
const row3 = $el("tr", {}, [td]);
|
||||
const row3 = $el('tr', {}, [td])
|
||||
|
||||
return $el("table", {}, [row1, row2, row3]);
|
||||
return $el('table', {}, [row1, row2, row3])
|
||||
} else {
|
||||
return [];
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
createImgPreview() {
|
||||
if (ComfyApp.clipspace.imgs) {
|
||||
return $el("img", { id: "clipspace_preview", ondragstart: () => false });
|
||||
} else return [];
|
||||
return $el('img', { id: 'clipspace_preview', ondragstart: () => false })
|
||||
} else return []
|
||||
}
|
||||
|
||||
show() {
|
||||
const img_preview = document.getElementById("clipspace_preview");
|
||||
ClipspaceDialog.invalidate();
|
||||
const img_preview = document.getElementById('clipspace_preview')
|
||||
ClipspaceDialog.invalidate()
|
||||
|
||||
this.element.style.display = "block";
|
||||
this.element.style.display = 'block'
|
||||
}
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.Clipspace",
|
||||
name: 'Comfy.Clipspace',
|
||||
init(app) {
|
||||
app.openClipspace = function () {
|
||||
if (!ClipspaceDialog.instance) {
|
||||
ClipspaceDialog.instance = new ClipspaceDialog();
|
||||
ComfyApp.clipspace_invalidate_handler = ClipspaceDialog.invalidate;
|
||||
ClipspaceDialog.instance = new ClipspaceDialog()
|
||||
ComfyApp.clipspace_invalidate_handler = ClipspaceDialog.invalidate
|
||||
}
|
||||
|
||||
if (ComfyApp.clipspace) {
|
||||
ClipspaceDialog.instance.show();
|
||||
} else app.ui.dialog.show("Clipspace is Empty!");
|
||||
};
|
||||
},
|
||||
});
|
||||
ClipspaceDialog.instance.show()
|
||||
} else app.ui.dialog.show('Clipspace is Empty!')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,172 +1,172 @@
|
||||
import { LiteGraph, LGraphCanvas } from "@comfyorg/litegraph";
|
||||
import { app } from "../../scripts/app";
|
||||
import { LiteGraph, LGraphCanvas } from '@comfyorg/litegraph'
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
// Adds filtering to combo context menus
|
||||
|
||||
const ext = {
|
||||
name: "Comfy.ContextMenuFilter",
|
||||
name: 'Comfy.ContextMenuFilter',
|
||||
init() {
|
||||
const ctxMenu = LiteGraph.ContextMenu;
|
||||
const ctxMenu = LiteGraph.ContextMenu
|
||||
// @ts-ignore
|
||||
// TODO Very hacky way to modify Litegraph behaviour. Fix this later.
|
||||
LiteGraph.ContextMenu = function (values, options) {
|
||||
const ctx = ctxMenu.call(this, values, options);
|
||||
const ctx = ctxMenu.call(this, values, options)
|
||||
|
||||
// If we are a dark menu (only used for combo boxes) then add a filter input
|
||||
if (options?.className === "dark" && values?.length > 10) {
|
||||
const filter = document.createElement("input");
|
||||
filter.classList.add("comfy-context-menu-filter");
|
||||
filter.placeholder = "Filter list";
|
||||
this.root.prepend(filter);
|
||||
if (options?.className === 'dark' && values?.length > 10) {
|
||||
const filter = document.createElement('input')
|
||||
filter.classList.add('comfy-context-menu-filter')
|
||||
filter.placeholder = 'Filter list'
|
||||
this.root.prepend(filter)
|
||||
|
||||
const items = Array.from(
|
||||
this.root.querySelectorAll(".litemenu-entry")
|
||||
) as HTMLElement[];
|
||||
let displayedItems = [...items];
|
||||
let itemCount = displayedItems.length;
|
||||
this.root.querySelectorAll('.litemenu-entry')
|
||||
) as HTMLElement[]
|
||||
let displayedItems = [...items]
|
||||
let itemCount = displayedItems.length
|
||||
|
||||
// We must request an animation frame for the current node of the active canvas to update.
|
||||
requestAnimationFrame(() => {
|
||||
// @ts-ignore
|
||||
const currentNode = LGraphCanvas.active_canvas.current_node;
|
||||
const currentNode = LGraphCanvas.active_canvas.current_node
|
||||
const clickedComboValue = currentNode.widgets
|
||||
?.filter(
|
||||
(w) =>
|
||||
w.type === "combo" && w.options.values.length === values.length
|
||||
w.type === 'combo' && w.options.values.length === values.length
|
||||
)
|
||||
.find((w) =>
|
||||
w.options.values.every((v, i) => v === values[i])
|
||||
)?.value;
|
||||
)?.value
|
||||
|
||||
let selectedIndex = clickedComboValue
|
||||
? values.findIndex((v) => v === clickedComboValue)
|
||||
: 0;
|
||||
: 0
|
||||
if (selectedIndex < 0) {
|
||||
selectedIndex = 0;
|
||||
selectedIndex = 0
|
||||
}
|
||||
let selectedItem = displayedItems[selectedIndex];
|
||||
updateSelected();
|
||||
let selectedItem = displayedItems[selectedIndex]
|
||||
updateSelected()
|
||||
|
||||
// Apply highlighting to the selected item
|
||||
function updateSelected() {
|
||||
selectedItem?.style.setProperty("background-color", "");
|
||||
selectedItem?.style.setProperty("color", "");
|
||||
selectedItem = displayedItems[selectedIndex];
|
||||
selectedItem?.style.setProperty('background-color', '')
|
||||
selectedItem?.style.setProperty('color', '')
|
||||
selectedItem = displayedItems[selectedIndex]
|
||||
selectedItem?.style.setProperty(
|
||||
"background-color",
|
||||
"#ccc",
|
||||
"important"
|
||||
);
|
||||
selectedItem?.style.setProperty("color", "#000", "important");
|
||||
'background-color',
|
||||
'#ccc',
|
||||
'important'
|
||||
)
|
||||
selectedItem?.style.setProperty('color', '#000', 'important')
|
||||
}
|
||||
|
||||
const positionList = () => {
|
||||
const rect = this.root.getBoundingClientRect();
|
||||
const rect = this.root.getBoundingClientRect()
|
||||
|
||||
// If the top is off-screen then shift the element with scaling applied
|
||||
if (rect.top < 0) {
|
||||
const scale =
|
||||
1 -
|
||||
this.root.getBoundingClientRect().height /
|
||||
this.root.clientHeight;
|
||||
const shift = (this.root.clientHeight * scale) / 2;
|
||||
this.root.style.top = -shift + "px";
|
||||
this.root.clientHeight
|
||||
const shift = (this.root.clientHeight * scale) / 2
|
||||
this.root.style.top = -shift + 'px'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Arrow up/down to select items
|
||||
filter.addEventListener("keydown", (event) => {
|
||||
filter.addEventListener('keydown', (event) => {
|
||||
switch (event.key) {
|
||||
case "ArrowUp":
|
||||
event.preventDefault();
|
||||
case 'ArrowUp':
|
||||
event.preventDefault()
|
||||
if (selectedIndex === 0) {
|
||||
selectedIndex = itemCount - 1;
|
||||
selectedIndex = itemCount - 1
|
||||
} else {
|
||||
selectedIndex--;
|
||||
selectedIndex--
|
||||
}
|
||||
updateSelected();
|
||||
break;
|
||||
case "ArrowRight":
|
||||
event.preventDefault();
|
||||
selectedIndex = itemCount - 1;
|
||||
updateSelected();
|
||||
break;
|
||||
case "ArrowDown":
|
||||
event.preventDefault();
|
||||
updateSelected()
|
||||
break
|
||||
case 'ArrowRight':
|
||||
event.preventDefault()
|
||||
selectedIndex = itemCount - 1
|
||||
updateSelected()
|
||||
break
|
||||
case 'ArrowDown':
|
||||
event.preventDefault()
|
||||
if (selectedIndex === itemCount - 1) {
|
||||
selectedIndex = 0;
|
||||
selectedIndex = 0
|
||||
} else {
|
||||
selectedIndex++;
|
||||
selectedIndex++
|
||||
}
|
||||
updateSelected();
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
event.preventDefault();
|
||||
selectedIndex = 0;
|
||||
updateSelected();
|
||||
break;
|
||||
case "Enter":
|
||||
selectedItem?.click();
|
||||
break;
|
||||
case "Escape":
|
||||
this.close();
|
||||
break;
|
||||
updateSelected()
|
||||
break
|
||||
case 'ArrowLeft':
|
||||
event.preventDefault()
|
||||
selectedIndex = 0
|
||||
updateSelected()
|
||||
break
|
||||
case 'Enter':
|
||||
selectedItem?.click()
|
||||
break
|
||||
case 'Escape':
|
||||
this.close()
|
||||
break
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
filter.addEventListener("input", () => {
|
||||
filter.addEventListener('input', () => {
|
||||
// Hide all items that don't match our filter
|
||||
const term = filter.value.toLocaleLowerCase();
|
||||
const term = filter.value.toLocaleLowerCase()
|
||||
// When filtering, recompute which items are visible for arrow up/down and maintain selection.
|
||||
displayedItems = items.filter((item) => {
|
||||
const isVisible =
|
||||
!term || item.textContent.toLocaleLowerCase().includes(term);
|
||||
item.style.display = isVisible ? "block" : "none";
|
||||
return isVisible;
|
||||
});
|
||||
!term || item.textContent.toLocaleLowerCase().includes(term)
|
||||
item.style.display = isVisible ? 'block' : 'none'
|
||||
return isVisible
|
||||
})
|
||||
|
||||
selectedIndex = 0;
|
||||
selectedIndex = 0
|
||||
if (displayedItems.includes(selectedItem)) {
|
||||
selectedIndex = displayedItems.findIndex(
|
||||
(d) => d === selectedItem
|
||||
);
|
||||
)
|
||||
}
|
||||
itemCount = displayedItems.length;
|
||||
itemCount = displayedItems.length
|
||||
|
||||
updateSelected();
|
||||
updateSelected()
|
||||
|
||||
// If we have an event then we can try and position the list under the source
|
||||
if (options.event) {
|
||||
let top = options.event.clientY - 10;
|
||||
let top = options.event.clientY - 10
|
||||
|
||||
const bodyRect = document.body.getBoundingClientRect();
|
||||
const rootRect = this.root.getBoundingClientRect();
|
||||
const bodyRect = document.body.getBoundingClientRect()
|
||||
const rootRect = this.root.getBoundingClientRect()
|
||||
if (
|
||||
bodyRect.height &&
|
||||
top > bodyRect.height - rootRect.height - 10
|
||||
) {
|
||||
top = Math.max(0, bodyRect.height - rootRect.height - 10);
|
||||
top = Math.max(0, bodyRect.height - rootRect.height - 10)
|
||||
}
|
||||
|
||||
this.root.style.top = top + "px";
|
||||
positionList();
|
||||
this.root.style.top = top + 'px'
|
||||
positionList()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
// Focus the filter box when opening
|
||||
filter.focus();
|
||||
filter.focus()
|
||||
|
||||
positionList();
|
||||
});
|
||||
});
|
||||
positionList()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return ctx;
|
||||
};
|
||||
return ctx
|
||||
}
|
||||
|
||||
LiteGraph.ContextMenu.prototype = ctxMenu.prototype;
|
||||
},
|
||||
};
|
||||
LiteGraph.ContextMenu.prototype = ctxMenu.prototype
|
||||
}
|
||||
}
|
||||
|
||||
app.registerExtension(ext);
|
||||
app.registerExtension(ext)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { app } from "../../scripts/app";
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
// Allows for simple dynamic prompt replacement
|
||||
// Inputs in the format {a|b} will have a random value of a or b chosen when the prompt is queued.
|
||||
@@ -7,46 +7,46 @@ import { app } from "../../scripts/app";
|
||||
* Strips C-style line and block comments from a string
|
||||
*/
|
||||
function stripComments(str) {
|
||||
return str.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "");
|
||||
return str.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '')
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.DynamicPrompts",
|
||||
name: 'Comfy.DynamicPrompts',
|
||||
nodeCreated(node) {
|
||||
if (node.widgets) {
|
||||
// Locate dynamic prompt text widgets
|
||||
// Include any widgets with dynamicPrompts set to true, and customtext
|
||||
const widgets = node.widgets.filter((n) => n.dynamicPrompts);
|
||||
const widgets = node.widgets.filter((n) => n.dynamicPrompts)
|
||||
for (const widget of widgets) {
|
||||
// Override the serialization of the value to resolve dynamic prompts for all widgets supporting it in this node
|
||||
widget.serializeValue = (workflowNode, widgetIndex) => {
|
||||
let prompt = stripComments(widget.value);
|
||||
let prompt = stripComments(widget.value)
|
||||
while (
|
||||
prompt.replace("\\{", "").includes("{") &&
|
||||
prompt.replace("\\}", "").includes("}")
|
||||
prompt.replace('\\{', '').includes('{') &&
|
||||
prompt.replace('\\}', '').includes('}')
|
||||
) {
|
||||
const startIndex = prompt.replace("\\{", "00").indexOf("{");
|
||||
const endIndex = prompt.replace("\\}", "00").indexOf("}");
|
||||
const startIndex = prompt.replace('\\{', '00').indexOf('{')
|
||||
const endIndex = prompt.replace('\\}', '00').indexOf('}')
|
||||
|
||||
const optionsString = prompt.substring(startIndex + 1, endIndex);
|
||||
const options = optionsString.split("|");
|
||||
const optionsString = prompt.substring(startIndex + 1, endIndex)
|
||||
const options = optionsString.split('|')
|
||||
|
||||
const randomIndex = Math.floor(Math.random() * options.length);
|
||||
const randomOption = options[randomIndex];
|
||||
const randomIndex = Math.floor(Math.random() * options.length)
|
||||
const randomOption = options[randomIndex]
|
||||
|
||||
prompt =
|
||||
prompt.substring(0, startIndex) +
|
||||
randomOption +
|
||||
prompt.substring(endIndex + 1);
|
||||
prompt.substring(endIndex + 1)
|
||||
}
|
||||
|
||||
// Overwrite the value in the serialized workflow pnginfo
|
||||
if (workflowNode?.widgets_values)
|
||||
workflowNode.widgets_values[widgetIndex] = prompt;
|
||||
workflowNode.widgets_values[widgetIndex] = prompt
|
||||
|
||||
return prompt;
|
||||
};
|
||||
return prompt
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,161 +1,161 @@
|
||||
import { app } from "../../scripts/app";
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
// Allows you to edit the attention weight by holding ctrl (or cmd) and using the up/down arrow keys
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.EditAttention",
|
||||
name: 'Comfy.EditAttention',
|
||||
init() {
|
||||
const editAttentionDelta = app.ui.settings.addSetting({
|
||||
id: "Comfy.EditAttention.Delta",
|
||||
name: "Ctrl+up/down precision",
|
||||
type: "slider",
|
||||
id: 'Comfy.EditAttention.Delta',
|
||||
name: 'Ctrl+up/down precision',
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 0.01,
|
||||
max: 0.5,
|
||||
step: 0.01,
|
||||
step: 0.01
|
||||
},
|
||||
defaultValue: 0.05,
|
||||
});
|
||||
defaultValue: 0.05
|
||||
})
|
||||
|
||||
function incrementWeight(weight, delta) {
|
||||
const floatWeight = parseFloat(weight);
|
||||
if (isNaN(floatWeight)) return weight;
|
||||
const newWeight = floatWeight + delta;
|
||||
if (newWeight < 0) return "0";
|
||||
return String(Number(newWeight.toFixed(10)));
|
||||
const floatWeight = parseFloat(weight)
|
||||
if (isNaN(floatWeight)) return weight
|
||||
const newWeight = floatWeight + delta
|
||||
if (newWeight < 0) return '0'
|
||||
return String(Number(newWeight.toFixed(10)))
|
||||
}
|
||||
|
||||
function findNearestEnclosure(text, cursorPos) {
|
||||
let start = cursorPos,
|
||||
end = cursorPos;
|
||||
end = cursorPos
|
||||
let openCount = 0,
|
||||
closeCount = 0;
|
||||
closeCount = 0
|
||||
|
||||
// Find opening parenthesis before cursor
|
||||
while (start >= 0) {
|
||||
start--;
|
||||
if (text[start] === "(" && openCount === closeCount) break;
|
||||
if (text[start] === "(") openCount++;
|
||||
if (text[start] === ")") closeCount++;
|
||||
start--
|
||||
if (text[start] === '(' && openCount === closeCount) break
|
||||
if (text[start] === '(') openCount++
|
||||
if (text[start] === ')') closeCount++
|
||||
}
|
||||
if (start < 0) return false;
|
||||
if (start < 0) return false
|
||||
|
||||
openCount = 0;
|
||||
closeCount = 0;
|
||||
openCount = 0
|
||||
closeCount = 0
|
||||
|
||||
// Find closing parenthesis after cursor
|
||||
while (end < text.length) {
|
||||
if (text[end] === ")" && openCount === closeCount) break;
|
||||
if (text[end] === "(") openCount++;
|
||||
if (text[end] === ")") closeCount++;
|
||||
end++;
|
||||
if (text[end] === ')' && openCount === closeCount) break
|
||||
if (text[end] === '(') openCount++
|
||||
if (text[end] === ')') closeCount++
|
||||
end++
|
||||
}
|
||||
if (end === text.length) return false;
|
||||
if (end === text.length) return false
|
||||
|
||||
return { start: start + 1, end: end };
|
||||
return { start: start + 1, end: end }
|
||||
}
|
||||
|
||||
function addWeightToParentheses(text) {
|
||||
const parenRegex = /^\((.*)\)$/;
|
||||
const parenMatch = text.match(parenRegex);
|
||||
const parenRegex = /^\((.*)\)$/
|
||||
const parenMatch = text.match(parenRegex)
|
||||
|
||||
const floatRegex = /:([+-]?(\d*\.)?\d+([eE][+-]?\d+)?)/;
|
||||
const floatMatch = text.match(floatRegex);
|
||||
const floatRegex = /:([+-]?(\d*\.)?\d+([eE][+-]?\d+)?)/
|
||||
const floatMatch = text.match(floatRegex)
|
||||
|
||||
if (parenMatch && !floatMatch) {
|
||||
return `(${parenMatch[1]}:1.0)`;
|
||||
return `(${parenMatch[1]}:1.0)`
|
||||
} else {
|
||||
return text;
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
function editAttention(event) {
|
||||
const inputField = event.composedPath()[0];
|
||||
const delta = parseFloat(editAttentionDelta.value);
|
||||
const inputField = event.composedPath()[0]
|
||||
const delta = parseFloat(editAttentionDelta.value)
|
||||
|
||||
if (inputField.tagName !== "TEXTAREA") return;
|
||||
if (!(event.key === "ArrowUp" || event.key === "ArrowDown")) return;
|
||||
if (!event.ctrlKey && !event.metaKey) return;
|
||||
if (inputField.tagName !== 'TEXTAREA') return
|
||||
if (!(event.key === 'ArrowUp' || event.key === 'ArrowDown')) return
|
||||
if (!event.ctrlKey && !event.metaKey) return
|
||||
|
||||
event.preventDefault();
|
||||
event.preventDefault()
|
||||
|
||||
let start = inputField.selectionStart;
|
||||
let end = inputField.selectionEnd;
|
||||
let selectedText = inputField.value.substring(start, end);
|
||||
let start = inputField.selectionStart
|
||||
let end = inputField.selectionEnd
|
||||
let selectedText = inputField.value.substring(start, end)
|
||||
|
||||
// If there is no selection, attempt to find the nearest enclosure, or select the current word
|
||||
if (!selectedText) {
|
||||
const nearestEnclosure = findNearestEnclosure(inputField.value, start);
|
||||
const nearestEnclosure = findNearestEnclosure(inputField.value, start)
|
||||
if (nearestEnclosure) {
|
||||
start = nearestEnclosure.start;
|
||||
end = nearestEnclosure.end;
|
||||
selectedText = inputField.value.substring(start, end);
|
||||
start = nearestEnclosure.start
|
||||
end = nearestEnclosure.end
|
||||
selectedText = inputField.value.substring(start, end)
|
||||
} else {
|
||||
// Select the current word, find the start and end of the word
|
||||
const delimiters = " .,\\/!?%^*;:{}=-_`~()\r\n\t";
|
||||
const delimiters = ' .,\\/!?%^*;:{}=-_`~()\r\n\t'
|
||||
|
||||
while (
|
||||
!delimiters.includes(inputField.value[start - 1]) &&
|
||||
start > 0
|
||||
) {
|
||||
start--;
|
||||
start--
|
||||
}
|
||||
|
||||
while (
|
||||
!delimiters.includes(inputField.value[end]) &&
|
||||
end < inputField.value.length
|
||||
) {
|
||||
end++;
|
||||
end++
|
||||
}
|
||||
|
||||
selectedText = inputField.value.substring(start, end);
|
||||
if (!selectedText) return;
|
||||
selectedText = inputField.value.substring(start, end)
|
||||
if (!selectedText) return
|
||||
}
|
||||
}
|
||||
|
||||
// If the selection ends with a space, remove it
|
||||
if (selectedText[selectedText.length - 1] === " ") {
|
||||
selectedText = selectedText.substring(0, selectedText.length - 1);
|
||||
end -= 1;
|
||||
if (selectedText[selectedText.length - 1] === ' ') {
|
||||
selectedText = selectedText.substring(0, selectedText.length - 1)
|
||||
end -= 1
|
||||
}
|
||||
|
||||
// If there are parentheses left and right of the selection, select them
|
||||
if (
|
||||
inputField.value[start - 1] === "(" &&
|
||||
inputField.value[end] === ")"
|
||||
inputField.value[start - 1] === '(' &&
|
||||
inputField.value[end] === ')'
|
||||
) {
|
||||
start -= 1;
|
||||
end += 1;
|
||||
selectedText = inputField.value.substring(start, end);
|
||||
start -= 1
|
||||
end += 1
|
||||
selectedText = inputField.value.substring(start, end)
|
||||
}
|
||||
|
||||
// If the selection is not enclosed in parentheses, add them
|
||||
if (
|
||||
selectedText[0] !== "(" ||
|
||||
selectedText[selectedText.length - 1] !== ")"
|
||||
selectedText[0] !== '(' ||
|
||||
selectedText[selectedText.length - 1] !== ')'
|
||||
) {
|
||||
selectedText = `(${selectedText})`;
|
||||
selectedText = `(${selectedText})`
|
||||
}
|
||||
|
||||
// If the selection does not have a weight, add a weight of 1.0
|
||||
selectedText = addWeightToParentheses(selectedText);
|
||||
selectedText = addWeightToParentheses(selectedText)
|
||||
|
||||
// Increment the weight
|
||||
const weightDelta = event.key === "ArrowUp" ? delta : -delta;
|
||||
const weightDelta = event.key === 'ArrowUp' ? delta : -delta
|
||||
const updatedText = selectedText.replace(
|
||||
/\((.*):(\d+(?:\.\d+)?)\)/,
|
||||
(match, text, weight) => {
|
||||
weight = incrementWeight(weight, weightDelta);
|
||||
weight = incrementWeight(weight, weightDelta)
|
||||
if (weight == 1) {
|
||||
return text;
|
||||
return text
|
||||
} else {
|
||||
return `(${text}:${weight})`;
|
||||
return `(${text}:${weight})`
|
||||
}
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
inputField.setRangeText(updatedText, start, end, "select");
|
||||
inputField.setRangeText(updatedText, start, end, 'select')
|
||||
}
|
||||
window.addEventListener("keydown", editAttention);
|
||||
},
|
||||
});
|
||||
window.addEventListener('keydown', editAttention)
|
||||
}
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,41 +1,41 @@
|
||||
import { $el, ComfyDialog } from "../../scripts/ui";
|
||||
import { DraggableList } from "../../scripts/ui/draggableList";
|
||||
import { GroupNodeConfig, GroupNodeHandler } from "./groupNode";
|
||||
import "./groupNodeManage.css";
|
||||
import { app, type ComfyApp } from "../../scripts/app";
|
||||
import { $el, ComfyDialog } from '../../scripts/ui'
|
||||
import { DraggableList } from '../../scripts/ui/draggableList'
|
||||
import { GroupNodeConfig, GroupNodeHandler } from './groupNode'
|
||||
import './groupNodeManage.css'
|
||||
import { app, type ComfyApp } from '../../scripts/app'
|
||||
import {
|
||||
LiteGraph,
|
||||
type LGraphNode,
|
||||
type LGraphNodeConstructor,
|
||||
} from "@comfyorg/litegraph";
|
||||
type LGraphNodeConstructor
|
||||
} from '@comfyorg/litegraph'
|
||||
|
||||
const ORDER: symbol = Symbol();
|
||||
const ORDER: symbol = Symbol()
|
||||
|
||||
function merge(target, source) {
|
||||
if (typeof target === "object" && typeof source === "object") {
|
||||
if (typeof target === 'object' && typeof source === 'object') {
|
||||
for (const key in source) {
|
||||
const sv = source[key];
|
||||
if (typeof sv === "object") {
|
||||
let tv = target[key];
|
||||
if (!tv) tv = target[key] = {};
|
||||
merge(tv, source[key]);
|
||||
const sv = source[key]
|
||||
if (typeof sv === 'object') {
|
||||
let tv = target[key]
|
||||
if (!tv) tv = target[key] = {}
|
||||
merge(tv, source[key])
|
||||
} else {
|
||||
target[key] = sv;
|
||||
target[key] = sv
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
return target
|
||||
}
|
||||
|
||||
export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
tabs: Record<
|
||||
"Inputs" | "Outputs" | "Widgets",
|
||||
'Inputs' | 'Outputs' | 'Widgets',
|
||||
{ tab: HTMLAnchorElement; page: HTMLElement }
|
||||
>;
|
||||
selectedNodeIndex: number | null | undefined;
|
||||
selectedTab: keyof ManageGroupDialog["tabs"] = "Inputs";
|
||||
selectedGroup: string | undefined;
|
||||
>
|
||||
selectedNodeIndex: number | null | undefined
|
||||
selectedTab: keyof ManageGroupDialog['tabs'] = 'Inputs'
|
||||
selectedGroup: string | undefined
|
||||
modifications: Record<
|
||||
string,
|
||||
Record<
|
||||
@@ -45,474 +45,472 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
{ name?: string | undefined; visible?: boolean | undefined }
|
||||
>
|
||||
>
|
||||
> = {};
|
||||
nodeItems: any[];
|
||||
app: ComfyApp;
|
||||
groupNodeType: LGraphNodeConstructor<LGraphNode>;
|
||||
groupNodeDef: any;
|
||||
groupData: any;
|
||||
> = {}
|
||||
nodeItems: any[]
|
||||
app: ComfyApp
|
||||
groupNodeType: LGraphNodeConstructor<LGraphNode>
|
||||
groupNodeDef: any
|
||||
groupData: any
|
||||
|
||||
innerNodesList: HTMLUListElement;
|
||||
widgetsPage: HTMLElement;
|
||||
inputsPage: HTMLElement;
|
||||
outputsPage: HTMLElement;
|
||||
draggable: any;
|
||||
innerNodesList: HTMLUListElement
|
||||
widgetsPage: HTMLElement
|
||||
inputsPage: HTMLElement
|
||||
outputsPage: HTMLElement
|
||||
draggable: any
|
||||
|
||||
get selectedNodeInnerIndex() {
|
||||
return +this.nodeItems[this.selectedNodeIndex].dataset.nodeindex;
|
||||
return +this.nodeItems[this.selectedNodeIndex].dataset.nodeindex
|
||||
}
|
||||
|
||||
constructor(app) {
|
||||
super();
|
||||
this.app = app;
|
||||
this.element = $el("dialog.comfy-group-manage", {
|
||||
parent: document.body,
|
||||
}) as HTMLDialogElement;
|
||||
super()
|
||||
this.app = app
|
||||
this.element = $el('dialog.comfy-group-manage', {
|
||||
parent: document.body
|
||||
}) as HTMLDialogElement
|
||||
}
|
||||
|
||||
changeTab(tab) {
|
||||
this.tabs[this.selectedTab].tab.classList.remove("active");
|
||||
this.tabs[this.selectedTab].page.classList.remove("active");
|
||||
this.tabs[tab].tab.classList.add("active");
|
||||
this.tabs[tab].page.classList.add("active");
|
||||
this.selectedTab = tab;
|
||||
this.tabs[this.selectedTab].tab.classList.remove('active')
|
||||
this.tabs[this.selectedTab].page.classList.remove('active')
|
||||
this.tabs[tab].tab.classList.add('active')
|
||||
this.tabs[tab].page.classList.add('active')
|
||||
this.selectedTab = tab
|
||||
}
|
||||
|
||||
changeNode(index, force?) {
|
||||
if (!force && this.selectedNodeIndex === index) return;
|
||||
if (!force && this.selectedNodeIndex === index) return
|
||||
|
||||
if (this.selectedNodeIndex != null) {
|
||||
this.nodeItems[this.selectedNodeIndex].classList.remove("selected");
|
||||
this.nodeItems[this.selectedNodeIndex].classList.remove('selected')
|
||||
}
|
||||
this.nodeItems[index].classList.add("selected");
|
||||
this.selectedNodeIndex = index;
|
||||
this.nodeItems[index].classList.add('selected')
|
||||
this.selectedNodeIndex = index
|
||||
|
||||
if (!this.buildInputsPage() && this.selectedTab === "Inputs") {
|
||||
this.changeTab("Widgets");
|
||||
if (!this.buildInputsPage() && this.selectedTab === 'Inputs') {
|
||||
this.changeTab('Widgets')
|
||||
}
|
||||
if (!this.buildWidgetsPage() && this.selectedTab === "Widgets") {
|
||||
this.changeTab("Outputs");
|
||||
if (!this.buildWidgetsPage() && this.selectedTab === 'Widgets') {
|
||||
this.changeTab('Outputs')
|
||||
}
|
||||
if (!this.buildOutputsPage() && this.selectedTab === "Outputs") {
|
||||
this.changeTab("Inputs");
|
||||
if (!this.buildOutputsPage() && this.selectedTab === 'Outputs') {
|
||||
this.changeTab('Inputs')
|
||||
}
|
||||
|
||||
this.changeTab(this.selectedTab);
|
||||
this.changeTab(this.selectedTab)
|
||||
}
|
||||
|
||||
getGroupData() {
|
||||
this.groupNodeType =
|
||||
LiteGraph.registered_node_types["workflow/" + this.selectedGroup];
|
||||
this.groupNodeDef = this.groupNodeType.nodeData;
|
||||
this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType);
|
||||
LiteGraph.registered_node_types['workflow/' + this.selectedGroup]
|
||||
this.groupNodeDef = this.groupNodeType.nodeData
|
||||
this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType)
|
||||
}
|
||||
|
||||
changeGroup(group, reset = true) {
|
||||
this.selectedGroup = group;
|
||||
this.getGroupData();
|
||||
this.selectedGroup = group
|
||||
this.getGroupData()
|
||||
|
||||
const nodes = this.groupData.nodeData.nodes;
|
||||
const nodes = this.groupData.nodeData.nodes
|
||||
this.nodeItems = nodes.map((n, i) =>
|
||||
$el(
|
||||
"li.draggable-item",
|
||||
'li.draggable-item',
|
||||
{
|
||||
dataset: {
|
||||
nodeindex: n.index + "",
|
||||
nodeindex: n.index + ''
|
||||
},
|
||||
onclick: () => {
|
||||
this.changeNode(i);
|
||||
},
|
||||
this.changeNode(i)
|
||||
}
|
||||
},
|
||||
[
|
||||
$el("span.drag-handle"),
|
||||
$el('span.drag-handle'),
|
||||
$el(
|
||||
"div",
|
||||
'div',
|
||||
{
|
||||
textContent: n.title ?? n.type,
|
||||
textContent: n.title ?? n.type
|
||||
},
|
||||
n.title
|
||||
? $el("span", {
|
||||
textContent: n.type,
|
||||
? $el('span', {
|
||||
textContent: n.type
|
||||
})
|
||||
: []
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
);
|
||||
)
|
||||
|
||||
this.innerNodesList.replaceChildren(...this.nodeItems);
|
||||
this.innerNodesList.replaceChildren(...this.nodeItems)
|
||||
|
||||
if (reset) {
|
||||
this.selectedNodeIndex = null;
|
||||
this.changeNode(0);
|
||||
this.selectedNodeIndex = null
|
||||
this.changeNode(0)
|
||||
} else {
|
||||
const items = this.draggable.getAllItems();
|
||||
let index = items.findIndex((item) =>
|
||||
item.classList.contains("selected")
|
||||
);
|
||||
if (index === -1) index = this.selectedNodeIndex;
|
||||
this.changeNode(index, true);
|
||||
const items = this.draggable.getAllItems()
|
||||
let index = items.findIndex((item) => item.classList.contains('selected'))
|
||||
if (index === -1) index = this.selectedNodeIndex
|
||||
this.changeNode(index, true)
|
||||
}
|
||||
|
||||
const ordered = [...nodes];
|
||||
this.draggable?.dispose();
|
||||
this.draggable = new DraggableList(this.innerNodesList, "li");
|
||||
const ordered = [...nodes]
|
||||
this.draggable?.dispose()
|
||||
this.draggable = new DraggableList(this.innerNodesList, 'li')
|
||||
this.draggable.addEventListener(
|
||||
"dragend",
|
||||
'dragend',
|
||||
({ detail: { oldPosition, newPosition } }) => {
|
||||
if (oldPosition === newPosition) return;
|
||||
ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0]);
|
||||
if (oldPosition === newPosition) return
|
||||
ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0])
|
||||
for (let i = 0; i < ordered.length; i++) {
|
||||
this.storeModification({
|
||||
nodeIndex: ordered[i].index,
|
||||
section: ORDER,
|
||||
prop: "order",
|
||||
value: i,
|
||||
});
|
||||
prop: 'order',
|
||||
value: i
|
||||
})
|
||||
}
|
||||
}
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
storeModification(props: {
|
||||
nodeIndex?: number;
|
||||
section: symbol;
|
||||
prop: string;
|
||||
value: any;
|
||||
nodeIndex?: number
|
||||
section: symbol
|
||||
prop: string
|
||||
value: any
|
||||
}) {
|
||||
const { nodeIndex, section, prop, value } = props;
|
||||
const groupMod = (this.modifications[this.selectedGroup] ??= {});
|
||||
const nodesMod = (groupMod.nodes ??= {});
|
||||
const nodeMod = (nodesMod[nodeIndex ?? this.selectedNodeInnerIndex] ??= {});
|
||||
const typeMod = (nodeMod[section] ??= {});
|
||||
if (typeof value === "object") {
|
||||
const objMod = (typeMod[prop] ??= {});
|
||||
Object.assign(objMod, value);
|
||||
const { nodeIndex, section, prop, value } = props
|
||||
const groupMod = (this.modifications[this.selectedGroup] ??= {})
|
||||
const nodesMod = (groupMod.nodes ??= {})
|
||||
const nodeMod = (nodesMod[nodeIndex ?? this.selectedNodeInnerIndex] ??= {})
|
||||
const typeMod = (nodeMod[section] ??= {})
|
||||
if (typeof value === 'object') {
|
||||
const objMod = (typeMod[prop] ??= {})
|
||||
Object.assign(objMod, value)
|
||||
} else {
|
||||
typeMod[prop] = value;
|
||||
typeMod[prop] = value
|
||||
}
|
||||
}
|
||||
|
||||
getEditElement(section, prop, value, placeholder, checked, checkable = true) {
|
||||
if (value === placeholder) value = "";
|
||||
if (value === placeholder) value = ''
|
||||
|
||||
const mods =
|
||||
this.modifications[this.selectedGroup]?.nodes?.[
|
||||
this.selectedNodeInnerIndex
|
||||
]?.[section]?.[prop];
|
||||
]?.[section]?.[prop]
|
||||
if (mods) {
|
||||
if (mods.name != null) {
|
||||
value = mods.name;
|
||||
value = mods.name
|
||||
}
|
||||
if (mods.visible != null) {
|
||||
checked = mods.visible;
|
||||
checked = mods.visible
|
||||
}
|
||||
}
|
||||
|
||||
return $el("div", [
|
||||
$el("input", {
|
||||
return $el('div', [
|
||||
$el('input', {
|
||||
value,
|
||||
placeholder,
|
||||
type: "text",
|
||||
type: 'text',
|
||||
onchange: (e) => {
|
||||
this.storeModification({
|
||||
section,
|
||||
prop,
|
||||
value: { name: e.target.value },
|
||||
});
|
||||
},
|
||||
value: { name: e.target.value }
|
||||
})
|
||||
}
|
||||
}),
|
||||
$el("label", { textContent: "Visible" }, [
|
||||
$el("input", {
|
||||
type: "checkbox",
|
||||
$el('label', { textContent: 'Visible' }, [
|
||||
$el('input', {
|
||||
type: 'checkbox',
|
||||
checked,
|
||||
disabled: !checkable,
|
||||
onchange: (e) => {
|
||||
this.storeModification({
|
||||
section,
|
||||
prop,
|
||||
value: { visible: !!e.target.checked },
|
||||
});
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
value: { visible: !!e.target.checked }
|
||||
})
|
||||
}
|
||||
})
|
||||
])
|
||||
])
|
||||
}
|
||||
|
||||
buildWidgetsPage() {
|
||||
const widgets =
|
||||
this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex];
|
||||
const items = Object.keys(widgets ?? {});
|
||||
const type = app.graph.extra.groupNodes[this.selectedGroup];
|
||||
const config = type.config?.[this.selectedNodeInnerIndex]?.input;
|
||||
this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex]
|
||||
const items = Object.keys(widgets ?? {})
|
||||
const type = app.graph.extra.groupNodes[this.selectedGroup]
|
||||
const config = type.config?.[this.selectedNodeInnerIndex]?.input
|
||||
this.widgetsPage.replaceChildren(
|
||||
...items.map((oldName) => {
|
||||
return this.getEditElement(
|
||||
"input",
|
||||
'input',
|
||||
oldName,
|
||||
widgets[oldName],
|
||||
oldName,
|
||||
config?.[oldName]?.visible !== false
|
||||
);
|
||||
)
|
||||
})
|
||||
);
|
||||
return !!items.length;
|
||||
)
|
||||
return !!items.length
|
||||
}
|
||||
|
||||
buildInputsPage() {
|
||||
const inputs = this.groupData.nodeInputs[this.selectedNodeInnerIndex];
|
||||
const items = Object.keys(inputs ?? {});
|
||||
const type = app.graph.extra.groupNodes[this.selectedGroup];
|
||||
const config = type.config?.[this.selectedNodeInnerIndex]?.input;
|
||||
const inputs = this.groupData.nodeInputs[this.selectedNodeInnerIndex]
|
||||
const items = Object.keys(inputs ?? {})
|
||||
const type = app.graph.extra.groupNodes[this.selectedGroup]
|
||||
const config = type.config?.[this.selectedNodeInnerIndex]?.input
|
||||
this.inputsPage.replaceChildren(
|
||||
...items
|
||||
.map((oldName) => {
|
||||
let value = inputs[oldName];
|
||||
let value = inputs[oldName]
|
||||
if (!value) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
return this.getEditElement(
|
||||
"input",
|
||||
'input',
|
||||
oldName,
|
||||
value,
|
||||
oldName,
|
||||
config?.[oldName]?.visible !== false
|
||||
);
|
||||
)
|
||||
})
|
||||
.filter(Boolean)
|
||||
);
|
||||
return !!items.length;
|
||||
)
|
||||
return !!items.length
|
||||
}
|
||||
|
||||
buildOutputsPage() {
|
||||
const nodes = this.groupData.nodeData.nodes;
|
||||
const nodes = this.groupData.nodeData.nodes
|
||||
const innerNodeDef = this.groupData.getNodeDef(
|
||||
nodes[this.selectedNodeInnerIndex]
|
||||
);
|
||||
const outputs = innerNodeDef?.output ?? [];
|
||||
)
|
||||
const outputs = innerNodeDef?.output ?? []
|
||||
const groupOutputs =
|
||||
this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex];
|
||||
this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex]
|
||||
|
||||
const type = app.graph.extra.groupNodes[this.selectedGroup];
|
||||
const config = type.config?.[this.selectedNodeInnerIndex]?.output;
|
||||
const node = this.groupData.nodeData.nodes[this.selectedNodeInnerIndex];
|
||||
const checkable = node.type !== "PrimitiveNode";
|
||||
const type = app.graph.extra.groupNodes[this.selectedGroup]
|
||||
const config = type.config?.[this.selectedNodeInnerIndex]?.output
|
||||
const node = this.groupData.nodeData.nodes[this.selectedNodeInnerIndex]
|
||||
const checkable = node.type !== 'PrimitiveNode'
|
||||
this.outputsPage.replaceChildren(
|
||||
...outputs
|
||||
.map((type, slot) => {
|
||||
const groupOutputIndex = groupOutputs?.[slot];
|
||||
const oldName = innerNodeDef.output_name?.[slot] ?? type;
|
||||
let value = config?.[slot]?.name;
|
||||
const visible = config?.[slot]?.visible || groupOutputIndex != null;
|
||||
const groupOutputIndex = groupOutputs?.[slot]
|
||||
const oldName = innerNodeDef.output_name?.[slot] ?? type
|
||||
let value = config?.[slot]?.name
|
||||
const visible = config?.[slot]?.visible || groupOutputIndex != null
|
||||
if (!value || value === oldName) {
|
||||
value = "";
|
||||
value = ''
|
||||
}
|
||||
return this.getEditElement(
|
||||
"output",
|
||||
'output',
|
||||
slot,
|
||||
value,
|
||||
oldName,
|
||||
visible,
|
||||
checkable
|
||||
);
|
||||
)
|
||||
})
|
||||
.filter(Boolean)
|
||||
);
|
||||
return !!outputs.length;
|
||||
)
|
||||
return !!outputs.length
|
||||
}
|
||||
|
||||
show(type?) {
|
||||
const groupNodes = Object.keys(app.graph.extra?.groupNodes ?? {}).sort(
|
||||
(a, b) => a.localeCompare(b)
|
||||
);
|
||||
)
|
||||
|
||||
this.innerNodesList = $el(
|
||||
"ul.comfy-group-manage-list-items"
|
||||
) as HTMLUListElement;
|
||||
this.widgetsPage = $el("section.comfy-group-manage-node-page");
|
||||
this.inputsPage = $el("section.comfy-group-manage-node-page");
|
||||
this.outputsPage = $el("section.comfy-group-manage-node-page");
|
||||
const pages = $el("div", [
|
||||
'ul.comfy-group-manage-list-items'
|
||||
) as HTMLUListElement
|
||||
this.widgetsPage = $el('section.comfy-group-manage-node-page')
|
||||
this.inputsPage = $el('section.comfy-group-manage-node-page')
|
||||
this.outputsPage = $el('section.comfy-group-manage-node-page')
|
||||
const pages = $el('div', [
|
||||
this.widgetsPage,
|
||||
this.inputsPage,
|
||||
this.outputsPage,
|
||||
]);
|
||||
this.outputsPage
|
||||
])
|
||||
|
||||
this.tabs = [
|
||||
["Inputs", this.inputsPage],
|
||||
["Widgets", this.widgetsPage],
|
||||
["Outputs", this.outputsPage],
|
||||
['Inputs', this.inputsPage],
|
||||
['Widgets', this.widgetsPage],
|
||||
['Outputs', this.outputsPage]
|
||||
].reduce((p, [name, page]: [string, HTMLElement]) => {
|
||||
p[name] = {
|
||||
tab: $el("a", {
|
||||
tab: $el('a', {
|
||||
onclick: () => {
|
||||
this.changeTab(name);
|
||||
this.changeTab(name)
|
||||
},
|
||||
textContent: name,
|
||||
textContent: name
|
||||
}),
|
||||
page,
|
||||
};
|
||||
return p;
|
||||
}, {}) as any;
|
||||
page
|
||||
}
|
||||
return p
|
||||
}, {}) as any
|
||||
|
||||
const outer = $el("div.comfy-group-manage-outer", [
|
||||
$el("header", [
|
||||
$el("h2", "Group Nodes"),
|
||||
const outer = $el('div.comfy-group-manage-outer', [
|
||||
$el('header', [
|
||||
$el('h2', 'Group Nodes'),
|
||||
$el(
|
||||
"select",
|
||||
'select',
|
||||
{
|
||||
onchange: (e) => {
|
||||
this.changeGroup(e.target.value);
|
||||
},
|
||||
this.changeGroup(e.target.value)
|
||||
}
|
||||
},
|
||||
groupNodes.map((g) =>
|
||||
$el("option", {
|
||||
$el('option', {
|
||||
textContent: g,
|
||||
selected: "workflow/" + g === type,
|
||||
value: g,
|
||||
selected: 'workflow/' + g === type,
|
||||
value: g
|
||||
})
|
||||
)
|
||||
),
|
||||
)
|
||||
]),
|
||||
$el("main", [
|
||||
$el("section.comfy-group-manage-list", this.innerNodesList),
|
||||
$el("section.comfy-group-manage-node", [
|
||||
$el('main', [
|
||||
$el('section.comfy-group-manage-list', this.innerNodesList),
|
||||
$el('section.comfy-group-manage-node', [
|
||||
$el(
|
||||
"header",
|
||||
'header',
|
||||
Object.values(this.tabs).map((t) => t.tab)
|
||||
),
|
||||
pages,
|
||||
pages
|
||||
])
|
||||
]),
|
||||
]),
|
||||
$el("footer", [
|
||||
$el('footer', [
|
||||
$el(
|
||||
"button.comfy-btn",
|
||||
'button.comfy-btn',
|
||||
{
|
||||
onclick: (e) => {
|
||||
// @ts-ignore
|
||||
const node = app.graph._nodes.find(
|
||||
(n) => n.type === "workflow/" + this.selectedGroup
|
||||
);
|
||||
(n) => n.type === 'workflow/' + this.selectedGroup
|
||||
)
|
||||
if (node) {
|
||||
alert(
|
||||
"This group node is in use in the current workflow, please first remove these."
|
||||
);
|
||||
return;
|
||||
'This group node is in use in the current workflow, please first remove these.'
|
||||
)
|
||||
return
|
||||
}
|
||||
if (
|
||||
confirm(
|
||||
`Are you sure you want to remove the node: "${this.selectedGroup}"`
|
||||
)
|
||||
) {
|
||||
delete app.graph.extra.groupNodes[this.selectedGroup];
|
||||
LiteGraph.unregisterNodeType("workflow/" + this.selectedGroup);
|
||||
delete app.graph.extra.groupNodes[this.selectedGroup]
|
||||
LiteGraph.unregisterNodeType('workflow/' + this.selectedGroup)
|
||||
}
|
||||
this.show()
|
||||
}
|
||||
this.show();
|
||||
},
|
||||
},
|
||||
"Delete Group Node"
|
||||
'Delete Group Node'
|
||||
),
|
||||
$el(
|
||||
"button.comfy-btn",
|
||||
'button.comfy-btn',
|
||||
{
|
||||
onclick: async () => {
|
||||
let nodesByType;
|
||||
let recreateNodes = [];
|
||||
const types = {};
|
||||
let nodesByType
|
||||
let recreateNodes = []
|
||||
const types = {}
|
||||
for (const g in this.modifications) {
|
||||
const type = app.graph.extra.groupNodes[g];
|
||||
let config = (type.config ??= {});
|
||||
const type = app.graph.extra.groupNodes[g]
|
||||
let config = (type.config ??= {})
|
||||
|
||||
let nodeMods = this.modifications[g]?.nodes;
|
||||
let nodeMods = this.modifications[g]?.nodes
|
||||
if (nodeMods) {
|
||||
const keys = Object.keys(nodeMods);
|
||||
const keys = Object.keys(nodeMods)
|
||||
if (nodeMods[keys[0]][ORDER]) {
|
||||
// If any node is reordered, they will all need sequencing
|
||||
const orderedNodes = [];
|
||||
const orderedMods = {};
|
||||
const orderedConfig = {};
|
||||
const orderedNodes = []
|
||||
const orderedMods = {}
|
||||
const orderedConfig = {}
|
||||
|
||||
for (const n of keys) {
|
||||
const order = nodeMods[n][ORDER].order;
|
||||
orderedNodes[order] = type.nodes[+n];
|
||||
orderedMods[order] = nodeMods[n];
|
||||
orderedNodes[order].index = order;
|
||||
const order = nodeMods[n][ORDER].order
|
||||
orderedNodes[order] = type.nodes[+n]
|
||||
orderedMods[order] = nodeMods[n]
|
||||
orderedNodes[order].index = order
|
||||
}
|
||||
|
||||
// Rewrite links
|
||||
for (const l of type.links) {
|
||||
if (l[0] != null) l[0] = type.nodes[l[0]].index;
|
||||
if (l[2] != null) l[2] = type.nodes[l[2]].index;
|
||||
if (l[0] != null) l[0] = type.nodes[l[0]].index
|
||||
if (l[2] != null) l[2] = type.nodes[l[2]].index
|
||||
}
|
||||
|
||||
// Rewrite externals
|
||||
if (type.external) {
|
||||
for (const ext of type.external) {
|
||||
ext[0] = type.nodes[ext[0]];
|
||||
ext[0] = type.nodes[ext[0]]
|
||||
}
|
||||
}
|
||||
|
||||
// Rewrite modifications
|
||||
for (const id of keys) {
|
||||
if (config[id]) {
|
||||
orderedConfig[type.nodes[id].index] = config[id];
|
||||
orderedConfig[type.nodes[id].index] = config[id]
|
||||
}
|
||||
delete config[id];
|
||||
delete config[id]
|
||||
}
|
||||
|
||||
type.nodes = orderedNodes;
|
||||
nodeMods = orderedMods;
|
||||
type.config = config = orderedConfig;
|
||||
type.nodes = orderedNodes
|
||||
nodeMods = orderedMods
|
||||
type.config = config = orderedConfig
|
||||
}
|
||||
|
||||
merge(config, nodeMods);
|
||||
merge(config, nodeMods)
|
||||
}
|
||||
|
||||
types[g] = type;
|
||||
types[g] = type
|
||||
|
||||
if (!nodesByType) {
|
||||
// @ts-ignore
|
||||
nodesByType = app.graph._nodes.reduce((p, n) => {
|
||||
p[n.type] ??= [];
|
||||
p[n.type].push(n);
|
||||
return p;
|
||||
}, {});
|
||||
p[n.type] ??= []
|
||||
p[n.type].push(n)
|
||||
return p
|
||||
}, {})
|
||||
}
|
||||
|
||||
const nodes = nodesByType["workflow/" + g];
|
||||
if (nodes) recreateNodes.push(...nodes);
|
||||
const nodes = nodesByType['workflow/' + g]
|
||||
if (nodes) recreateNodes.push(...nodes)
|
||||
}
|
||||
|
||||
await GroupNodeConfig.registerFromWorkflow(types, {});
|
||||
await GroupNodeConfig.registerFromWorkflow(types, {})
|
||||
|
||||
for (const node of recreateNodes) {
|
||||
node.recreate();
|
||||
node.recreate()
|
||||
}
|
||||
|
||||
this.modifications = {};
|
||||
this.app.graph.setDirtyCanvas(true, true);
|
||||
this.changeGroup(this.selectedGroup, false);
|
||||
this.modifications = {}
|
||||
this.app.graph.setDirtyCanvas(true, true)
|
||||
this.changeGroup(this.selectedGroup, false)
|
||||
}
|
||||
},
|
||||
},
|
||||
"Save"
|
||||
'Save'
|
||||
),
|
||||
$el(
|
||||
"button.comfy-btn",
|
||||
'button.comfy-btn',
|
||||
{ onclick: () => this.element.close() },
|
||||
"Close"
|
||||
),
|
||||
]),
|
||||
]);
|
||||
'Close'
|
||||
)
|
||||
])
|
||||
])
|
||||
|
||||
this.element.replaceChildren(outer);
|
||||
this.element.replaceChildren(outer)
|
||||
this.changeGroup(
|
||||
type ? groupNodes.find((g) => "workflow/" + g === type) : groupNodes[0]
|
||||
);
|
||||
this.element.showModal();
|
||||
type ? groupNodes.find((g) => 'workflow/' + g === type) : groupNodes[0]
|
||||
)
|
||||
this.element.showModal()
|
||||
|
||||
this.element.addEventListener("close", () => {
|
||||
this.draggable?.dispose();
|
||||
});
|
||||
this.element.addEventListener('close', () => {
|
||||
this.draggable?.dispose()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,141 +1,141 @@
|
||||
import { app } from "../../scripts/app";
|
||||
import { LGraphCanvas, LiteGraph } from "@comfyorg/litegraph";
|
||||
import { app } from '../../scripts/app'
|
||||
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
||||
|
||||
function setNodeMode(node, mode) {
|
||||
node.mode = mode;
|
||||
node.graph.change();
|
||||
node.mode = mode
|
||||
node.graph.change()
|
||||
}
|
||||
|
||||
function addNodesToGroup(group, nodes = []) {
|
||||
var x1, y1, x2, y2;
|
||||
var nx1, ny1, nx2, ny2;
|
||||
var node;
|
||||
var x1, y1, x2, y2
|
||||
var nx1, ny1, nx2, ny2
|
||||
var node
|
||||
|
||||
x1 = y1 = x2 = y2 = -1;
|
||||
nx1 = ny1 = nx2 = ny2 = -1;
|
||||
x1 = y1 = x2 = y2 = -1
|
||||
nx1 = ny1 = nx2 = ny2 = -1
|
||||
|
||||
for (var n of [group._nodes, nodes]) {
|
||||
for (var i in n) {
|
||||
node = n[i];
|
||||
node = n[i]
|
||||
|
||||
nx1 = node.pos[0];
|
||||
ny1 = node.pos[1];
|
||||
nx2 = node.pos[0] + node.size[0];
|
||||
ny2 = node.pos[1] + node.size[1];
|
||||
nx1 = node.pos[0]
|
||||
ny1 = node.pos[1]
|
||||
nx2 = node.pos[0] + node.size[0]
|
||||
ny2 = node.pos[1] + node.size[1]
|
||||
|
||||
if (node.type != "Reroute") {
|
||||
ny1 -= LiteGraph.NODE_TITLE_HEIGHT;
|
||||
if (node.type != 'Reroute') {
|
||||
ny1 -= LiteGraph.NODE_TITLE_HEIGHT
|
||||
}
|
||||
|
||||
if (node.flags?.collapsed) {
|
||||
ny2 = ny1 + LiteGraph.NODE_TITLE_HEIGHT;
|
||||
ny2 = ny1 + LiteGraph.NODE_TITLE_HEIGHT
|
||||
|
||||
if (node?._collapsed_width) {
|
||||
nx2 = nx1 + Math.round(node._collapsed_width);
|
||||
nx2 = nx1 + Math.round(node._collapsed_width)
|
||||
}
|
||||
}
|
||||
|
||||
if (x1 == -1 || nx1 < x1) {
|
||||
x1 = nx1;
|
||||
x1 = nx1
|
||||
}
|
||||
|
||||
if (y1 == -1 || ny1 < y1) {
|
||||
y1 = ny1;
|
||||
y1 = ny1
|
||||
}
|
||||
|
||||
if (x2 == -1 || nx2 > x2) {
|
||||
x2 = nx2;
|
||||
x2 = nx2
|
||||
}
|
||||
|
||||
if (y2 == -1 || ny2 > y2) {
|
||||
y2 = ny2;
|
||||
y2 = ny2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var padding = 10;
|
||||
var padding = 10
|
||||
|
||||
y1 = y1 - Math.round(group.font_size * 1.4);
|
||||
y1 = y1 - Math.round(group.font_size * 1.4)
|
||||
|
||||
group.pos = [x1 - padding, y1 - padding];
|
||||
group.size = [x2 - x1 + padding * 2, y2 - y1 + padding * 2];
|
||||
group.pos = [x1 - padding, y1 - padding]
|
||||
group.size = [x2 - x1 + padding * 2, y2 - y1 + padding * 2]
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.GroupOptions",
|
||||
name: 'Comfy.GroupOptions',
|
||||
setup() {
|
||||
// @ts-ignore
|
||||
const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
|
||||
const orig = LGraphCanvas.prototype.getCanvasMenuOptions
|
||||
// graph_mouse
|
||||
// @ts-ignore
|
||||
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
|
||||
const options = orig.apply(this, arguments);
|
||||
const options = orig.apply(this, arguments)
|
||||
const group = this.graph.getGroupOnPos(
|
||||
this.graph_mouse[0],
|
||||
this.graph_mouse[1]
|
||||
);
|
||||
)
|
||||
if (!group) {
|
||||
options.push({
|
||||
content: "Add Group For Selected Nodes",
|
||||
content: 'Add Group For Selected Nodes',
|
||||
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
|
||||
callback: () => {
|
||||
// @ts-ignore
|
||||
var group = new LiteGraph.LGraphGroup();
|
||||
addNodesToGroup(group, this.selected_nodes);
|
||||
app.canvas.graph.add(group);
|
||||
this.graph.change();
|
||||
},
|
||||
});
|
||||
var group = new LiteGraph.LGraphGroup()
|
||||
addNodesToGroup(group, this.selected_nodes)
|
||||
app.canvas.graph.add(group)
|
||||
this.graph.change()
|
||||
}
|
||||
})
|
||||
|
||||
return options;
|
||||
return options
|
||||
}
|
||||
|
||||
// Group nodes aren't recomputed until the group is moved, this ensures the nodes are up-to-date
|
||||
group.recomputeInsideNodes();
|
||||
const nodesInGroup = group._nodes;
|
||||
group.recomputeInsideNodes()
|
||||
const nodesInGroup = group._nodes
|
||||
|
||||
options.push({
|
||||
content: "Add Selected Nodes To Group",
|
||||
content: 'Add Selected Nodes To Group',
|
||||
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
|
||||
callback: () => {
|
||||
addNodesToGroup(group, this.selected_nodes);
|
||||
this.graph.change();
|
||||
},
|
||||
});
|
||||
addNodesToGroup(group, this.selected_nodes)
|
||||
this.graph.change()
|
||||
}
|
||||
})
|
||||
|
||||
// No nodes in group, return default options
|
||||
if (nodesInGroup.length === 0) {
|
||||
return options;
|
||||
return options
|
||||
} else {
|
||||
// Add a separator between the default options and the group options
|
||||
options.push(null);
|
||||
options.push(null)
|
||||
}
|
||||
|
||||
// Check if all nodes are the same mode
|
||||
let allNodesAreSameMode = true;
|
||||
let allNodesAreSameMode = true
|
||||
for (let i = 1; i < nodesInGroup.length; i++) {
|
||||
if (nodesInGroup[i].mode !== nodesInGroup[0].mode) {
|
||||
allNodesAreSameMode = false;
|
||||
break;
|
||||
allNodesAreSameMode = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
options.push({
|
||||
content: "Fit Group To Nodes",
|
||||
content: 'Fit Group To Nodes',
|
||||
callback: () => {
|
||||
addNodesToGroup(group);
|
||||
this.graph.change();
|
||||
},
|
||||
});
|
||||
addNodesToGroup(group)
|
||||
this.graph.change()
|
||||
}
|
||||
})
|
||||
|
||||
options.push({
|
||||
content: "Select Nodes",
|
||||
content: 'Select Nodes',
|
||||
callback: () => {
|
||||
this.selectNodes(nodesInGroup);
|
||||
this.graph.change();
|
||||
this.canvas.focus();
|
||||
},
|
||||
});
|
||||
this.selectNodes(nodesInGroup)
|
||||
this.graph.change()
|
||||
this.canvas.focus()
|
||||
}
|
||||
})
|
||||
|
||||
// Modes
|
||||
// 0: Always
|
||||
@@ -145,122 +145,122 @@ app.registerExtension({
|
||||
// 4: Bypass
|
||||
// If all nodes are the same mode, add a menu option to change the mode
|
||||
if (allNodesAreSameMode) {
|
||||
const mode = nodesInGroup[0].mode;
|
||||
const mode = nodesInGroup[0].mode
|
||||
switch (mode) {
|
||||
case 0:
|
||||
// All nodes are always, option to disable, and bypass
|
||||
options.push({
|
||||
content: "Set Group Nodes to Never",
|
||||
content: 'Set Group Nodes to Never',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 2);
|
||||
setNodeMode(node, 2)
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
options.push({
|
||||
content: "Bypass Group Nodes",
|
||||
content: 'Bypass Group Nodes',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 4);
|
||||
setNodeMode(node, 4)
|
||||
}
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
})
|
||||
break
|
||||
case 2:
|
||||
// All nodes are never, option to enable, and bypass
|
||||
options.push({
|
||||
content: "Set Group Nodes to Always",
|
||||
content: 'Set Group Nodes to Always',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 0);
|
||||
setNodeMode(node, 0)
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
options.push({
|
||||
content: "Bypass Group Nodes",
|
||||
content: 'Bypass Group Nodes',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 4);
|
||||
setNodeMode(node, 4)
|
||||
}
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
})
|
||||
break
|
||||
case 4:
|
||||
// All nodes are bypass, option to enable, and disable
|
||||
options.push({
|
||||
content: "Set Group Nodes to Always",
|
||||
content: 'Set Group Nodes to Always',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 0);
|
||||
setNodeMode(node, 0)
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
options.push({
|
||||
content: "Set Group Nodes to Never",
|
||||
content: 'Set Group Nodes to Never',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 2);
|
||||
setNodeMode(node, 2)
|
||||
}
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
})
|
||||
break
|
||||
default:
|
||||
// All nodes are On Trigger or On Event(Or other?), option to disable, set to always, or bypass
|
||||
options.push({
|
||||
content: "Set Group Nodes to Always",
|
||||
content: 'Set Group Nodes to Always',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 0);
|
||||
setNodeMode(node, 0)
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
options.push({
|
||||
content: "Set Group Nodes to Never",
|
||||
content: 'Set Group Nodes to Never',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 2);
|
||||
setNodeMode(node, 2)
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
options.push({
|
||||
content: "Bypass Group Nodes",
|
||||
content: 'Bypass Group Nodes',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 4);
|
||||
setNodeMode(node, 4)
|
||||
}
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// Nodes are not all the same mode, add a menu option to change the mode to always, never, or bypass
|
||||
options.push({
|
||||
content: "Set Group Nodes to Always",
|
||||
content: 'Set Group Nodes to Always',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 0);
|
||||
setNodeMode(node, 0)
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
options.push({
|
||||
content: "Set Group Nodes to Never",
|
||||
content: 'Set Group Nodes to Never',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 2);
|
||||
setNodeMode(node, 2)
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
options.push({
|
||||
content: "Bypass Group Nodes",
|
||||
content: 'Bypass Group Nodes',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 4);
|
||||
setNodeMode(node, 4)
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
},
|
||||
});
|
||||
return options
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import "./clipspace";
|
||||
import "./colorPalette";
|
||||
import "./contextMenuFilter";
|
||||
import "./dynamicPrompts";
|
||||
import "./editAttention";
|
||||
import "./groupNode";
|
||||
import "./groupNodeManage";
|
||||
import "./groupOptions";
|
||||
import "./invertMenuScrolling";
|
||||
import "./keybinds";
|
||||
import "./linkRenderMode";
|
||||
import "./maskeditor";
|
||||
import "./nodeTemplates";
|
||||
import "./noteNode";
|
||||
import "./rerouteNode";
|
||||
import "./saveImageExtraOutput";
|
||||
import "./simpleTouchSupport";
|
||||
import "./slotDefaults";
|
||||
import "./snapToGrid";
|
||||
import "./uploadImage";
|
||||
import "./webcamCapture";
|
||||
import "./widgetInputs";
|
||||
import "./uploadAudio";
|
||||
import './clipspace'
|
||||
import './colorPalette'
|
||||
import './contextMenuFilter'
|
||||
import './dynamicPrompts'
|
||||
import './editAttention'
|
||||
import './groupNode'
|
||||
import './groupNodeManage'
|
||||
import './groupOptions'
|
||||
import './invertMenuScrolling'
|
||||
import './keybinds'
|
||||
import './linkRenderMode'
|
||||
import './maskeditor'
|
||||
import './nodeTemplates'
|
||||
import './noteNode'
|
||||
import './rerouteNode'
|
||||
import './saveImageExtraOutput'
|
||||
import './simpleTouchSupport'
|
||||
import './slotDefaults'
|
||||
import './snapToGrid'
|
||||
import './uploadImage'
|
||||
import './webcamCapture'
|
||||
import './widgetInputs'
|
||||
import './uploadAudio'
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import { LiteGraph } from "@comfyorg/litegraph";
|
||||
import { app } from "../../scripts/app";
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
// Inverts the scrolling of context menus
|
||||
|
||||
const id = "Comfy.InvertMenuScrolling";
|
||||
const id = 'Comfy.InvertMenuScrolling'
|
||||
app.registerExtension({
|
||||
name: id,
|
||||
init() {
|
||||
const ctxMenu = LiteGraph.ContextMenu;
|
||||
const ctxMenu = LiteGraph.ContextMenu
|
||||
const replace = () => {
|
||||
// @ts-ignore
|
||||
LiteGraph.ContextMenu = function (values, options) {
|
||||
options = options || {};
|
||||
options = options || {}
|
||||
if (options.scroll_speed) {
|
||||
options.scroll_speed *= -1;
|
||||
options.scroll_speed *= -1
|
||||
} else {
|
||||
options.scroll_speed = -0.1;
|
||||
options.scroll_speed = -0.1
|
||||
}
|
||||
return ctxMenu.call(this, values, options)
|
||||
}
|
||||
LiteGraph.ContextMenu.prototype = ctxMenu.prototype
|
||||
}
|
||||
return ctxMenu.call(this, values, options);
|
||||
};
|
||||
LiteGraph.ContextMenu.prototype = ctxMenu.prototype;
|
||||
};
|
||||
app.ui.settings.addSetting({
|
||||
id,
|
||||
name: "Invert Menu Scrolling",
|
||||
type: "boolean",
|
||||
name: 'Invert Menu Scrolling',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
onChange(value) {
|
||||
if (value) {
|
||||
replace();
|
||||
replace()
|
||||
} else {
|
||||
LiteGraph.ContextMenu = ctxMenu;
|
||||
LiteGraph.ContextMenu = ctxMenu
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,73 +1,73 @@
|
||||
import { app } from "../../scripts/app";
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.Keybinds",
|
||||
name: 'Comfy.Keybinds',
|
||||
init() {
|
||||
const keybindListener = function (event) {
|
||||
const modifierPressed = event.ctrlKey || event.metaKey;
|
||||
const modifierPressed = event.ctrlKey || event.metaKey
|
||||
|
||||
// Queue prompt using ctrl or command + enter
|
||||
if (modifierPressed && event.key === "Enter") {
|
||||
app.queuePrompt(event.shiftKey ? -1 : 0).then();
|
||||
return;
|
||||
if (modifierPressed && event.key === 'Enter') {
|
||||
app.queuePrompt(event.shiftKey ? -1 : 0).then()
|
||||
return
|
||||
}
|
||||
|
||||
const target = event.composedPath()[0];
|
||||
if (["INPUT", "TEXTAREA"].includes(target.tagName)) {
|
||||
return;
|
||||
const target = event.composedPath()[0]
|
||||
if (['INPUT', 'TEXTAREA'].includes(target.tagName)) {
|
||||
return
|
||||
}
|
||||
|
||||
const modifierKeyIdMap = {
|
||||
s: "#comfy-save-button",
|
||||
o: "#comfy-file-input",
|
||||
Backspace: "#comfy-clear-button",
|
||||
d: "#comfy-load-default-button",
|
||||
};
|
||||
s: '#comfy-save-button',
|
||||
o: '#comfy-file-input',
|
||||
Backspace: '#comfy-clear-button',
|
||||
d: '#comfy-load-default-button'
|
||||
}
|
||||
|
||||
const modifierKeybindId = modifierKeyIdMap[event.key];
|
||||
const modifierKeybindId = modifierKeyIdMap[event.key]
|
||||
if (modifierPressed && modifierKeybindId) {
|
||||
event.preventDefault();
|
||||
event.preventDefault()
|
||||
|
||||
const elem = document.querySelector(modifierKeybindId);
|
||||
elem.click();
|
||||
return;
|
||||
const elem = document.querySelector(modifierKeybindId)
|
||||
elem.click()
|
||||
return
|
||||
}
|
||||
|
||||
// Finished Handling all modifier keybinds, now handle the rest
|
||||
if (event.ctrlKey || event.altKey || event.metaKey) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
// Close out of modals using escape
|
||||
if (event.key === "Escape") {
|
||||
const modals = document.querySelectorAll<HTMLElement>(".comfy-modal");
|
||||
if (event.key === 'Escape') {
|
||||
const modals = document.querySelectorAll<HTMLElement>('.comfy-modal')
|
||||
const modal = Array.from(modals).find(
|
||||
(modal) =>
|
||||
window.getComputedStyle(modal).getPropertyValue("display") !==
|
||||
"none"
|
||||
);
|
||||
window.getComputedStyle(modal).getPropertyValue('display') !==
|
||||
'none'
|
||||
)
|
||||
if (modal) {
|
||||
modal.style.display = "none";
|
||||
modal.style.display = 'none'
|
||||
}
|
||||
|
||||
[...document.querySelectorAll("dialog")].forEach((d) => {
|
||||
d.close();
|
||||
});
|
||||
;[...document.querySelectorAll('dialog')].forEach((d) => {
|
||||
d.close()
|
||||
})
|
||||
}
|
||||
|
||||
const keyIdMap = {
|
||||
q: "#comfy-view-queue-button",
|
||||
h: "#comfy-view-history-button",
|
||||
r: "#comfy-refresh-button",
|
||||
};
|
||||
|
||||
const buttonId = keyIdMap[event.key];
|
||||
if (buttonId) {
|
||||
const button = document.querySelector(buttonId);
|
||||
button.click();
|
||||
q: '#comfy-view-queue-button',
|
||||
h: '#comfy-view-history-button',
|
||||
r: '#comfy-refresh-button'
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", keybindListener, true);
|
||||
},
|
||||
});
|
||||
const buttonId = keyIdMap[event.key]
|
||||
if (buttonId) {
|
||||
const button = document.querySelector(buttonId)
|
||||
button.click()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', keybindListener, true)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { app } from "../../scripts/app";
|
||||
import { LiteGraph } from "@comfyorg/litegraph";
|
||||
const id = "Comfy.LinkRenderMode";
|
||||
import { app } from '../../scripts/app'
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
const id = 'Comfy.LinkRenderMode'
|
||||
const ext = {
|
||||
name: id,
|
||||
async setup(app) {
|
||||
app.ui.settings.addSetting({
|
||||
id,
|
||||
name: "Link Render Mode",
|
||||
name: 'Link Render Mode',
|
||||
defaultValue: 2,
|
||||
type: "combo",
|
||||
type: 'combo',
|
||||
// @ts-ignore
|
||||
options: [...LiteGraph.LINK_RENDER_MODES, "Hidden"].map((m, i) => ({
|
||||
options: [...LiteGraph.LINK_RENDER_MODES, 'Hidden'].map((m, i) => ({
|
||||
value: i,
|
||||
text: m,
|
||||
selected: i == app.canvas.links_render_mode,
|
||||
selected: i == app.canvas.links_render_mode
|
||||
})),
|
||||
onChange(value) {
|
||||
app.canvas.links_render_mode = +value;
|
||||
app.graph.setDirtyCanvas(true);
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
app.canvas.links_render_mode = +value
|
||||
app.graph.setDirtyCanvas(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
app.registerExtension(ext);
|
||||
app.registerExtension(ext)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
import { app } from "../../scripts/app";
|
||||
import { api } from "../../scripts/api";
|
||||
import { ComfyDialog, $el } from "../../scripts/ui";
|
||||
import { GroupNodeConfig, GroupNodeHandler } from "./groupNode";
|
||||
import { LGraphCanvas } from "@comfyorg/litegraph";
|
||||
import { app } from '../../scripts/app'
|
||||
import { api } from '../../scripts/api'
|
||||
import { ComfyDialog, $el } from '../../scripts/ui'
|
||||
import { GroupNodeConfig, GroupNodeHandler } from './groupNode'
|
||||
import { LGraphCanvas } from '@comfyorg/litegraph'
|
||||
|
||||
// Adds the ability to save and add multiple nodes as a template
|
||||
// To save:
|
||||
@@ -21,391 +21,391 @@ import { LGraphCanvas } from "@comfyorg/litegraph";
|
||||
// To rearrange:
|
||||
// Open the manage dialog and Drag and drop elements using the "Name:" label as handle
|
||||
|
||||
const id = "Comfy.NodeTemplates";
|
||||
const file = "comfy.templates.json";
|
||||
const id = 'Comfy.NodeTemplates'
|
||||
const file = 'comfy.templates.json'
|
||||
|
||||
class ManageTemplates extends ComfyDialog {
|
||||
templates: any[];
|
||||
draggedEl: HTMLElement | null;
|
||||
saveVisualCue: number | null;
|
||||
emptyImg: HTMLImageElement;
|
||||
importInput: HTMLInputElement;
|
||||
templates: any[]
|
||||
draggedEl: HTMLElement | null
|
||||
saveVisualCue: number | null
|
||||
emptyImg: HTMLImageElement
|
||||
importInput: HTMLInputElement
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
super()
|
||||
this.load().then((v) => {
|
||||
this.templates = v;
|
||||
});
|
||||
this.templates = v
|
||||
})
|
||||
|
||||
this.element.classList.add("comfy-manage-templates");
|
||||
this.draggedEl = null;
|
||||
this.saveVisualCue = null;
|
||||
this.emptyImg = new Image();
|
||||
this.element.classList.add('comfy-manage-templates')
|
||||
this.draggedEl = null
|
||||
this.saveVisualCue = null
|
||||
this.emptyImg = new Image()
|
||||
this.emptyImg.src =
|
||||
"data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=";
|
||||
'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='
|
||||
|
||||
this.importInput = $el("input", {
|
||||
type: "file",
|
||||
accept: ".json",
|
||||
this.importInput = $el('input', {
|
||||
type: 'file',
|
||||
accept: '.json',
|
||||
multiple: true,
|
||||
style: { display: "none" },
|
||||
style: { display: 'none' },
|
||||
parent: document.body,
|
||||
onchange: () => this.importAll(),
|
||||
}) as HTMLInputElement;
|
||||
onchange: () => this.importAll()
|
||||
}) as HTMLInputElement
|
||||
}
|
||||
|
||||
createButtons() {
|
||||
const btns = super.createButtons();
|
||||
btns[0].textContent = "Close";
|
||||
const btns = super.createButtons()
|
||||
btns[0].textContent = 'Close'
|
||||
btns[0].onclick = (e) => {
|
||||
clearTimeout(this.saveVisualCue);
|
||||
this.close();
|
||||
};
|
||||
clearTimeout(this.saveVisualCue)
|
||||
this.close()
|
||||
}
|
||||
btns.unshift(
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Export",
|
||||
onclick: () => this.exportAll(),
|
||||
$el('button', {
|
||||
type: 'button',
|
||||
textContent: 'Export',
|
||||
onclick: () => this.exportAll()
|
||||
})
|
||||
);
|
||||
)
|
||||
btns.unshift(
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Import",
|
||||
$el('button', {
|
||||
type: 'button',
|
||||
textContent: 'Import',
|
||||
onclick: () => {
|
||||
this.importInput.click();
|
||||
},
|
||||
this.importInput.click()
|
||||
}
|
||||
})
|
||||
);
|
||||
return btns;
|
||||
)
|
||||
return btns
|
||||
}
|
||||
|
||||
async load() {
|
||||
let templates = [];
|
||||
if (app.storageLocation === "server") {
|
||||
let templates = []
|
||||
if (app.storageLocation === 'server') {
|
||||
if (app.isNewUserSession) {
|
||||
// New user so migrate existing templates
|
||||
const json = localStorage.getItem(id);
|
||||
const json = localStorage.getItem(id)
|
||||
if (json) {
|
||||
templates = JSON.parse(json);
|
||||
templates = JSON.parse(json)
|
||||
}
|
||||
await api.storeUserData(file, json, { stringify: false });
|
||||
await api.storeUserData(file, json, { stringify: false })
|
||||
} else {
|
||||
const res = await api.getUserData(file);
|
||||
const res = await api.getUserData(file)
|
||||
if (res.status === 200) {
|
||||
try {
|
||||
templates = await res.json();
|
||||
templates = await res.json()
|
||||
} catch (error) {}
|
||||
} else if (res.status !== 404) {
|
||||
console.error(res.status + " " + res.statusText);
|
||||
console.error(res.status + ' ' + res.statusText)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const json = localStorage.getItem(id);
|
||||
const json = localStorage.getItem(id)
|
||||
if (json) {
|
||||
templates = JSON.parse(json);
|
||||
templates = JSON.parse(json)
|
||||
}
|
||||
}
|
||||
|
||||
return templates ?? [];
|
||||
return templates ?? []
|
||||
}
|
||||
|
||||
async store() {
|
||||
if (app.storageLocation === "server") {
|
||||
const templates = JSON.stringify(this.templates, undefined, 4);
|
||||
localStorage.setItem(id, templates); // Backwards compatibility
|
||||
if (app.storageLocation === 'server') {
|
||||
const templates = JSON.stringify(this.templates, undefined, 4)
|
||||
localStorage.setItem(id, templates) // Backwards compatibility
|
||||
try {
|
||||
await api.storeUserData(file, templates, { stringify: false });
|
||||
await api.storeUserData(file, templates, { stringify: false })
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert(error.message);
|
||||
console.error(error)
|
||||
alert(error.message)
|
||||
}
|
||||
} else {
|
||||
localStorage.setItem(id, JSON.stringify(this.templates));
|
||||
localStorage.setItem(id, JSON.stringify(this.templates))
|
||||
}
|
||||
}
|
||||
|
||||
async importAll() {
|
||||
for (const file of this.importInput.files) {
|
||||
if (file.type === "application/json" || file.name.endsWith(".json")) {
|
||||
const reader = new FileReader();
|
||||
if (file.type === 'application/json' || file.name.endsWith('.json')) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = async () => {
|
||||
const importFile = JSON.parse(reader.result as string);
|
||||
const importFile = JSON.parse(reader.result as string)
|
||||
if (importFile?.templates) {
|
||||
for (const template of importFile.templates) {
|
||||
if (template?.name && template?.data) {
|
||||
this.templates.push(template);
|
||||
this.templates.push(template)
|
||||
}
|
||||
}
|
||||
await this.store();
|
||||
await this.store()
|
||||
}
|
||||
};
|
||||
await reader.readAsText(file);
|
||||
}
|
||||
await reader.readAsText(file)
|
||||
}
|
||||
}
|
||||
|
||||
this.importInput.value = null;
|
||||
this.importInput.value = null
|
||||
|
||||
this.close();
|
||||
this.close()
|
||||
}
|
||||
|
||||
exportAll() {
|
||||
if (this.templates.length == 0) {
|
||||
alert("No templates to export.");
|
||||
return;
|
||||
alert('No templates to export.')
|
||||
return
|
||||
}
|
||||
|
||||
const json = JSON.stringify({ templates: this.templates }, null, 2); // convert the data to a JSON string
|
||||
const blob = new Blob([json], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = $el("a", {
|
||||
const json = JSON.stringify({ templates: this.templates }, null, 2) // convert the data to a JSON string
|
||||
const blob = new Blob([json], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = $el('a', {
|
||||
href: url,
|
||||
download: "node_templates.json",
|
||||
style: { display: "none" },
|
||||
parent: document.body,
|
||||
});
|
||||
a.click();
|
||||
download: 'node_templates.json',
|
||||
style: { display: 'none' },
|
||||
parent: document.body
|
||||
})
|
||||
a.click()
|
||||
setTimeout(function () {
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
a.remove()
|
||||
window.URL.revokeObjectURL(url)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
show() {
|
||||
// Show list of template names + delete button
|
||||
super.show(
|
||||
$el(
|
||||
"div",
|
||||
'div',
|
||||
{},
|
||||
this.templates.flatMap((t, i) => {
|
||||
let nameInput;
|
||||
let nameInput
|
||||
return [
|
||||
$el(
|
||||
"div",
|
||||
'div',
|
||||
{
|
||||
dataset: { id: i.toString() },
|
||||
className: "tempateManagerRow",
|
||||
className: 'tempateManagerRow',
|
||||
style: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr auto",
|
||||
border: "1px dashed transparent",
|
||||
gap: "5px",
|
||||
backgroundColor: "var(--comfy-menu-bg)",
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr auto',
|
||||
border: '1px dashed transparent',
|
||||
gap: '5px',
|
||||
backgroundColor: 'var(--comfy-menu-bg)'
|
||||
},
|
||||
ondragstart: (e) => {
|
||||
this.draggedEl = e.currentTarget;
|
||||
e.currentTarget.style.opacity = "0.6";
|
||||
e.currentTarget.style.border = "1px dashed yellow";
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
e.dataTransfer.setDragImage(this.emptyImg, 0, 0);
|
||||
this.draggedEl = e.currentTarget
|
||||
e.currentTarget.style.opacity = '0.6'
|
||||
e.currentTarget.style.border = '1px dashed yellow'
|
||||
e.dataTransfer.effectAllowed = 'move'
|
||||
e.dataTransfer.setDragImage(this.emptyImg, 0, 0)
|
||||
},
|
||||
ondragend: (e) => {
|
||||
e.target.style.opacity = "1";
|
||||
e.currentTarget.style.border = "1px dashed transparent";
|
||||
e.currentTarget.removeAttribute("draggable");
|
||||
e.target.style.opacity = '1'
|
||||
e.currentTarget.style.border = '1px dashed transparent'
|
||||
e.currentTarget.removeAttribute('draggable')
|
||||
|
||||
// rearrange the elements
|
||||
this.element
|
||||
.querySelectorAll(".tempateManagerRow")
|
||||
.querySelectorAll('.tempateManagerRow')
|
||||
.forEach((el: HTMLElement, i) => {
|
||||
var prev_i = Number.parseInt(el.dataset.id);
|
||||
var prev_i = Number.parseInt(el.dataset.id)
|
||||
|
||||
if (el == this.draggedEl && prev_i != i) {
|
||||
this.templates.splice(
|
||||
i,
|
||||
0,
|
||||
this.templates.splice(prev_i, 1)[0]
|
||||
);
|
||||
)
|
||||
}
|
||||
el.dataset.id = i.toString();
|
||||
});
|
||||
this.store();
|
||||
el.dataset.id = i.toString()
|
||||
})
|
||||
this.store()
|
||||
},
|
||||
ondragover: (e) => {
|
||||
e.preventDefault();
|
||||
if (e.currentTarget == this.draggedEl) return;
|
||||
e.preventDefault()
|
||||
if (e.currentTarget == this.draggedEl) return
|
||||
|
||||
let rect = e.currentTarget.getBoundingClientRect();
|
||||
let rect = e.currentTarget.getBoundingClientRect()
|
||||
if (e.clientY > rect.top + rect.height / 2) {
|
||||
e.currentTarget.parentNode.insertBefore(
|
||||
this.draggedEl,
|
||||
e.currentTarget.nextSibling
|
||||
);
|
||||
)
|
||||
} else {
|
||||
e.currentTarget.parentNode.insertBefore(
|
||||
this.draggedEl,
|
||||
e.currentTarget
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
[
|
||||
$el(
|
||||
"label",
|
||||
'label',
|
||||
{
|
||||
textContent: "Name: ",
|
||||
textContent: 'Name: ',
|
||||
style: {
|
||||
cursor: "grab",
|
||||
cursor: 'grab'
|
||||
},
|
||||
onmousedown: (e) => {
|
||||
// enable dragging only from the label
|
||||
if (e.target.localName == "label")
|
||||
e.currentTarget.parentNode.draggable = "true";
|
||||
},
|
||||
if (e.target.localName == 'label')
|
||||
e.currentTarget.parentNode.draggable = 'true'
|
||||
}
|
||||
},
|
||||
[
|
||||
$el("input", {
|
||||
$el('input', {
|
||||
value: t.name,
|
||||
dataset: { name: t.name },
|
||||
style: {
|
||||
transitionProperty: "background-color",
|
||||
transitionDuration: "0s",
|
||||
transitionProperty: 'background-color',
|
||||
transitionDuration: '0s'
|
||||
},
|
||||
onchange: (e) => {
|
||||
clearTimeout(this.saveVisualCue);
|
||||
var el = e.target;
|
||||
var row = el.parentNode.parentNode;
|
||||
clearTimeout(this.saveVisualCue)
|
||||
var el = e.target
|
||||
var row = el.parentNode.parentNode
|
||||
this.templates[row.dataset.id].name =
|
||||
el.value.trim() || "untitled";
|
||||
this.store();
|
||||
el.style.backgroundColor = "rgb(40, 95, 40)";
|
||||
el.style.transitionDuration = "0s";
|
||||
el.value.trim() || 'untitled'
|
||||
this.store()
|
||||
el.style.backgroundColor = 'rgb(40, 95, 40)'
|
||||
el.style.transitionDuration = '0s'
|
||||
// @ts-expect-error
|
||||
// In browser env the return value is number.
|
||||
this.saveVisualCue = setTimeout(function () {
|
||||
el.style.transitionDuration = ".7s";
|
||||
el.style.backgroundColor = "var(--comfy-input-bg)";
|
||||
}, 15);
|
||||
el.style.transitionDuration = '.7s'
|
||||
el.style.backgroundColor = 'var(--comfy-input-bg)'
|
||||
}, 15)
|
||||
},
|
||||
onkeypress: (e) => {
|
||||
var el = e.target;
|
||||
clearTimeout(this.saveVisualCue);
|
||||
el.style.transitionDuration = "0s";
|
||||
el.style.backgroundColor = "var(--comfy-input-bg)";
|
||||
var el = e.target
|
||||
clearTimeout(this.saveVisualCue)
|
||||
el.style.transitionDuration = '0s'
|
||||
el.style.backgroundColor = 'var(--comfy-input-bg)'
|
||||
},
|
||||
$: (el) => (nameInput = el),
|
||||
}),
|
||||
$: (el) => (nameInput = el)
|
||||
})
|
||||
]
|
||||
),
|
||||
$el("div", {}, [
|
||||
$el("button", {
|
||||
textContent: "Export",
|
||||
$el('div', {}, [
|
||||
$el('button', {
|
||||
textContent: 'Export',
|
||||
style: {
|
||||
fontSize: "12px",
|
||||
fontWeight: "normal",
|
||||
fontSize: '12px',
|
||||
fontWeight: 'normal'
|
||||
},
|
||||
onclick: (e) => {
|
||||
const json = JSON.stringify({ templates: [t] }, null, 2); // convert the data to a JSON string
|
||||
const json = JSON.stringify({ templates: [t] }, null, 2) // convert the data to a JSON string
|
||||
const blob = new Blob([json], {
|
||||
type: "application/json",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = $el("a", {
|
||||
type: 'application/json'
|
||||
})
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = $el('a', {
|
||||
href: url,
|
||||
download: (nameInput.value || t.name) + ".json",
|
||||
style: { display: "none" },
|
||||
parent: document.body,
|
||||
});
|
||||
a.click();
|
||||
download: (nameInput.value || t.name) + '.json',
|
||||
style: { display: 'none' },
|
||||
parent: document.body
|
||||
})
|
||||
a.click()
|
||||
setTimeout(function () {
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
},
|
||||
a.remove()
|
||||
window.URL.revokeObjectURL(url)
|
||||
}, 0)
|
||||
}
|
||||
}),
|
||||
$el("button", {
|
||||
textContent: "Delete",
|
||||
$el('button', {
|
||||
textContent: 'Delete',
|
||||
style: {
|
||||
fontSize: "12px",
|
||||
color: "red",
|
||||
fontWeight: "normal",
|
||||
fontSize: '12px',
|
||||
color: 'red',
|
||||
fontWeight: 'normal'
|
||||
},
|
||||
onclick: (e) => {
|
||||
const item = e.target.parentNode.parentNode;
|
||||
item.parentNode.removeChild(item);
|
||||
this.templates.splice(item.dataset.id * 1, 1);
|
||||
this.store();
|
||||
const item = e.target.parentNode.parentNode
|
||||
item.parentNode.removeChild(item)
|
||||
this.templates.splice(item.dataset.id * 1, 1)
|
||||
this.store()
|
||||
// update the rows index, setTimeout ensures that the list is updated
|
||||
var that = this;
|
||||
var that = this
|
||||
setTimeout(function () {
|
||||
that.element
|
||||
.querySelectorAll(".tempateManagerRow")
|
||||
.querySelectorAll('.tempateManagerRow')
|
||||
.forEach((el: HTMLElement, i) => {
|
||||
el.dataset.id = i.toString();
|
||||
});
|
||||
}, 0);
|
||||
},
|
||||
}),
|
||||
]),
|
||||
el.dataset.id = i.toString()
|
||||
})
|
||||
}, 0)
|
||||
}
|
||||
})
|
||||
])
|
||||
]
|
||||
)
|
||||
]
|
||||
),
|
||||
];
|
||||
})
|
||||
)
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: id,
|
||||
setup() {
|
||||
const manage = new ManageTemplates();
|
||||
const manage = new ManageTemplates()
|
||||
|
||||
const clipboardAction = async (cb) => {
|
||||
// We use the clipboard functions but dont want to overwrite the current user clipboard
|
||||
// Restore it after we've run our callback
|
||||
const old = localStorage.getItem("litegrapheditor_clipboard");
|
||||
await cb();
|
||||
localStorage.setItem("litegrapheditor_clipboard", old);
|
||||
};
|
||||
const old = localStorage.getItem('litegrapheditor_clipboard')
|
||||
await cb()
|
||||
localStorage.setItem('litegrapheditor_clipboard', old)
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
|
||||
const orig = LGraphCanvas.prototype.getCanvasMenuOptions
|
||||
// @ts-ignore
|
||||
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
|
||||
const options = orig.apply(this, arguments);
|
||||
const options = orig.apply(this, arguments)
|
||||
|
||||
options.push(null);
|
||||
options.push(null)
|
||||
options.push({
|
||||
content: `Save Selected as Template`,
|
||||
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
|
||||
callback: () => {
|
||||
const name = prompt("Enter name");
|
||||
if (!name?.trim()) return;
|
||||
const name = prompt('Enter name')
|
||||
if (!name?.trim()) return
|
||||
|
||||
clipboardAction(() => {
|
||||
app.canvas.copyToClipboard();
|
||||
let data = localStorage.getItem("litegrapheditor_clipboard");
|
||||
data = JSON.parse(data);
|
||||
const nodeIds = Object.keys(app.canvas.selected_nodes);
|
||||
app.canvas.copyToClipboard()
|
||||
let data = localStorage.getItem('litegrapheditor_clipboard')
|
||||
data = JSON.parse(data)
|
||||
const nodeIds = Object.keys(app.canvas.selected_nodes)
|
||||
for (let i = 0; i < nodeIds.length; i++) {
|
||||
const node = app.graph.getNodeById(Number.parseInt(nodeIds[i]));
|
||||
const node = app.graph.getNodeById(Number.parseInt(nodeIds[i]))
|
||||
// @ts-ignore
|
||||
const nodeData = node?.constructor.nodeData;
|
||||
const nodeData = node?.constructor.nodeData
|
||||
|
||||
let groupData = GroupNodeHandler.getGroupData(node);
|
||||
let groupData = GroupNodeHandler.getGroupData(node)
|
||||
if (groupData) {
|
||||
groupData = groupData.nodeData;
|
||||
groupData = groupData.nodeData
|
||||
// @ts-ignore
|
||||
if (!data.groupNodes) {
|
||||
// @ts-ignore
|
||||
data.groupNodes = {};
|
||||
data.groupNodes = {}
|
||||
}
|
||||
// @ts-ignore
|
||||
data.groupNodes[nodeData.name] = groupData;
|
||||
data.groupNodes[nodeData.name] = groupData
|
||||
// @ts-ignore
|
||||
data.nodes[i].type = nodeData.name;
|
||||
data.nodes[i].type = nodeData.name
|
||||
}
|
||||
}
|
||||
|
||||
manage.templates.push({
|
||||
name,
|
||||
data: JSON.stringify(data),
|
||||
});
|
||||
manage.store();
|
||||
});
|
||||
},
|
||||
});
|
||||
data: JSON.stringify(data)
|
||||
})
|
||||
manage.store()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Map each template to a menu item
|
||||
const subItems = manage.templates.map((t) => {
|
||||
@@ -413,28 +413,28 @@ app.registerExtension({
|
||||
content: t.name,
|
||||
callback: () => {
|
||||
clipboardAction(async () => {
|
||||
const data = JSON.parse(t.data);
|
||||
await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {});
|
||||
localStorage.setItem("litegrapheditor_clipboard", t.data);
|
||||
app.canvas.pasteFromClipboard();
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
const data = JSON.parse(t.data)
|
||||
await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {})
|
||||
localStorage.setItem('litegrapheditor_clipboard', t.data)
|
||||
app.canvas.pasteFromClipboard()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
subItems.push(null, {
|
||||
content: "Manage",
|
||||
callback: () => manage.show(),
|
||||
});
|
||||
content: 'Manage',
|
||||
callback: () => manage.show()
|
||||
})
|
||||
|
||||
options.push({
|
||||
content: "Node Templates",
|
||||
content: 'Node Templates',
|
||||
submenu: {
|
||||
options: subItems,
|
||||
},
|
||||
});
|
||||
options: subItems
|
||||
}
|
||||
})
|
||||
|
||||
return options;
|
||||
};
|
||||
},
|
||||
});
|
||||
return options
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
import { LiteGraph, LGraphCanvas } from "@comfyorg/litegraph";
|
||||
import { app } from "../../scripts/app";
|
||||
import { ComfyWidgets } from "../../scripts/widgets";
|
||||
import { LiteGraph, LGraphCanvas } from '@comfyorg/litegraph'
|
||||
import { app } from '../../scripts/app'
|
||||
import { ComfyWidgets } from '../../scripts/widgets'
|
||||
// Node that add notes to your project
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.NoteNode",
|
||||
name: 'Comfy.NoteNode',
|
||||
registerCustomNodes() {
|
||||
class NoteNode {
|
||||
static category: string;
|
||||
static category: string
|
||||
|
||||
color = LGraphCanvas.node_colors.yellow.color;
|
||||
bgcolor = LGraphCanvas.node_colors.yellow.bgcolor;
|
||||
groupcolor = LGraphCanvas.node_colors.yellow.groupcolor;
|
||||
properties: { text: string };
|
||||
serialize_widgets: boolean;
|
||||
isVirtualNode: boolean;
|
||||
collapsable: boolean;
|
||||
title_mode: number;
|
||||
color = LGraphCanvas.node_colors.yellow.color
|
||||
bgcolor = LGraphCanvas.node_colors.yellow.bgcolor
|
||||
groupcolor = LGraphCanvas.node_colors.yellow.groupcolor
|
||||
properties: { text: string }
|
||||
serialize_widgets: boolean
|
||||
isVirtualNode: boolean
|
||||
collapsable: boolean
|
||||
title_mode: number
|
||||
|
||||
constructor() {
|
||||
if (!this.properties) {
|
||||
this.properties = { text: "" };
|
||||
this.properties = { text: '' }
|
||||
}
|
||||
ComfyWidgets.STRING(
|
||||
// @ts-ignore
|
||||
// Should we extends LGraphNode?
|
||||
this,
|
||||
"",
|
||||
["", { default: this.properties.text, multiline: true }],
|
||||
'',
|
||||
['', { default: this.properties.text, multiline: true }],
|
||||
app
|
||||
);
|
||||
)
|
||||
|
||||
this.serialize_widgets = true;
|
||||
this.isVirtualNode = true;
|
||||
this.serialize_widgets = true
|
||||
this.isVirtualNode = true
|
||||
}
|
||||
}
|
||||
|
||||
// Load default visibility
|
||||
|
||||
LiteGraph.registerNodeType(
|
||||
"Note",
|
||||
'Note',
|
||||
// @ts-ignore
|
||||
Object.assign(NoteNode, {
|
||||
title_mode: LiteGraph.NORMAL_TITLE,
|
||||
title: "Note",
|
||||
collapsable: true,
|
||||
title: 'Note',
|
||||
collapsable: true
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
NoteNode.category = "utils";
|
||||
},
|
||||
});
|
||||
NoteNode.category = 'utils'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
import { app } from "../../scripts/app";
|
||||
import { mergeIfValid, getWidgetConfig, setWidgetConfig } from "./widgetInputs";
|
||||
import { LiteGraph, LGraphCanvas, LGraphNode } from "@comfyorg/litegraph";
|
||||
import { app } from '../../scripts/app'
|
||||
import { mergeIfValid, getWidgetConfig, setWidgetConfig } from './widgetInputs'
|
||||
import { LiteGraph, LGraphCanvas, LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
// Node that allows you to redirect connections for cleaner graphs
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.RerouteNode",
|
||||
name: 'Comfy.RerouteNode',
|
||||
registerCustomNodes(app) {
|
||||
interface RerouteNode extends LGraphNode {
|
||||
__outputType?: string;
|
||||
__outputType?: string
|
||||
}
|
||||
|
||||
class RerouteNode {
|
||||
static category: string | undefined;
|
||||
static defaultVisibility = false;
|
||||
static category: string | undefined
|
||||
static defaultVisibility = false
|
||||
|
||||
constructor() {
|
||||
if (!this.properties) {
|
||||
this.properties = {};
|
||||
this.properties = {}
|
||||
}
|
||||
this.properties.showOutputText = RerouteNode.defaultVisibility;
|
||||
this.properties.horizontal = false;
|
||||
this.properties.showOutputText = RerouteNode.defaultVisibility
|
||||
this.properties.horizontal = false
|
||||
|
||||
this.addInput("", "*");
|
||||
this.addOutput(this.properties.showOutputText ? "*" : "", "*");
|
||||
this.addInput('', '*')
|
||||
this.addOutput(this.properties.showOutputText ? '*' : '', '*')
|
||||
|
||||
this.onAfterGraphConfigured = function () {
|
||||
requestAnimationFrame(() => {
|
||||
this.onConnectionsChange(LiteGraph.INPUT, null, true, null);
|
||||
});
|
||||
};
|
||||
this.onConnectionsChange(LiteGraph.INPUT, null, true, null)
|
||||
})
|
||||
}
|
||||
|
||||
this.onConnectionsChange = function (
|
||||
type,
|
||||
@@ -37,7 +37,7 @@ app.registerExtension({
|
||||
connected,
|
||||
link_info
|
||||
) {
|
||||
this.applyOrientation();
|
||||
this.applyOrientation()
|
||||
|
||||
// Prevent multiple connections to different types when we have no input
|
||||
if (connected && type === LiteGraph.OUTPUT) {
|
||||
@@ -45,78 +45,78 @@ app.registerExtension({
|
||||
const types = new Set(
|
||||
this.outputs[0].links
|
||||
.map((l) => app.graph.links[l].type)
|
||||
.filter((t) => t !== "*")
|
||||
);
|
||||
.filter((t) => t !== '*')
|
||||
)
|
||||
if (types.size > 1) {
|
||||
const linksToDisconnect = [];
|
||||
const linksToDisconnect = []
|
||||
for (let i = 0; i < this.outputs[0].links.length - 1; i++) {
|
||||
const linkId = this.outputs[0].links[i];
|
||||
const link = app.graph.links[linkId];
|
||||
linksToDisconnect.push(link);
|
||||
const linkId = this.outputs[0].links[i]
|
||||
const link = app.graph.links[linkId]
|
||||
linksToDisconnect.push(link)
|
||||
}
|
||||
for (const link of linksToDisconnect) {
|
||||
const node = app.graph.getNodeById(link.target_id);
|
||||
node.disconnectInput(link.target_slot);
|
||||
const node = app.graph.getNodeById(link.target_id)
|
||||
node.disconnectInput(link.target_slot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find root input
|
||||
let currentNode = this;
|
||||
let updateNodes = [];
|
||||
let inputType = null;
|
||||
let inputNode = null;
|
||||
let currentNode = this
|
||||
let updateNodes = []
|
||||
let inputType = null
|
||||
let inputNode = null
|
||||
while (currentNode) {
|
||||
updateNodes.unshift(currentNode);
|
||||
const linkId = currentNode.inputs[0].link;
|
||||
updateNodes.unshift(currentNode)
|
||||
const linkId = currentNode.inputs[0].link
|
||||
if (linkId !== null) {
|
||||
const link = app.graph.links[linkId];
|
||||
if (!link) return;
|
||||
const node = app.graph.getNodeById(link.origin_id);
|
||||
const type = node.constructor.type;
|
||||
if (type === "Reroute") {
|
||||
const link = app.graph.links[linkId]
|
||||
if (!link) return
|
||||
const node = app.graph.getNodeById(link.origin_id)
|
||||
const type = node.constructor.type
|
||||
if (type === 'Reroute') {
|
||||
if (node === this) {
|
||||
// We've found a circle
|
||||
currentNode.disconnectInput(link.target_slot);
|
||||
currentNode = null;
|
||||
currentNode.disconnectInput(link.target_slot)
|
||||
currentNode = null
|
||||
} else {
|
||||
// Move the previous node
|
||||
currentNode = node;
|
||||
currentNode = node
|
||||
}
|
||||
} else {
|
||||
// We've found the end
|
||||
inputNode = currentNode;
|
||||
inputType = node.outputs[link.origin_slot]?.type ?? null;
|
||||
break;
|
||||
inputNode = currentNode
|
||||
inputType = node.outputs[link.origin_slot]?.type ?? null
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// This path has no input node
|
||||
currentNode = null;
|
||||
break;
|
||||
currentNode = null
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Find all outputs
|
||||
const nodes = [this];
|
||||
let outputType = null;
|
||||
const nodes = [this]
|
||||
let outputType = null
|
||||
while (nodes.length) {
|
||||
currentNode = nodes.pop();
|
||||
currentNode = nodes.pop()
|
||||
const outputs =
|
||||
(currentNode.outputs ? currentNode.outputs[0].links : []) || [];
|
||||
(currentNode.outputs ? currentNode.outputs[0].links : []) || []
|
||||
if (outputs.length) {
|
||||
for (const linkId of outputs) {
|
||||
const link = app.graph.links[linkId];
|
||||
const link = app.graph.links[linkId]
|
||||
|
||||
// When disconnecting sometimes the link is still registered
|
||||
if (!link) continue;
|
||||
if (!link) continue
|
||||
|
||||
const node = app.graph.getNodeById(link.target_id);
|
||||
const type = node.constructor.type;
|
||||
const node = app.graph.getNodeById(link.target_id)
|
||||
const type = node.constructor.type
|
||||
|
||||
if (type === "Reroute") {
|
||||
if (type === 'Reroute') {
|
||||
// Follow reroute nodes
|
||||
nodes.push(node);
|
||||
updateNodes.push(node);
|
||||
nodes.push(node)
|
||||
updateNodes.push(node)
|
||||
} else {
|
||||
// We've found an output
|
||||
const nodeOutType =
|
||||
@@ -124,16 +124,16 @@ app.registerExtension({
|
||||
node.inputs[link?.target_slot] &&
|
||||
node.inputs[link.target_slot].type
|
||||
? node.inputs[link.target_slot].type
|
||||
: null;
|
||||
: null
|
||||
if (
|
||||
inputType &&
|
||||
inputType !== "*" &&
|
||||
inputType !== '*' &&
|
||||
nodeOutType !== inputType
|
||||
) {
|
||||
// The output doesnt match our input so disconnect it
|
||||
node.disconnectInput(link.target_slot);
|
||||
node.disconnectInput(link.target_slot)
|
||||
} else {
|
||||
outputType = nodeOutType;
|
||||
outputType = nodeOutType
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,50 +142,50 @@ app.registerExtension({
|
||||
}
|
||||
}
|
||||
|
||||
const displayType = inputType || outputType || "*";
|
||||
const color = LGraphCanvas.link_type_colors[displayType];
|
||||
const displayType = inputType || outputType || '*'
|
||||
const color = LGraphCanvas.link_type_colors[displayType]
|
||||
|
||||
let widgetConfig;
|
||||
let targetWidget;
|
||||
let widgetType;
|
||||
let widgetConfig
|
||||
let targetWidget
|
||||
let widgetType
|
||||
// Update the types of each node
|
||||
for (const node of updateNodes) {
|
||||
// If we dont have an input type we are always wildcard but we'll show the output type
|
||||
// This lets you change the output link to a different type and all nodes will update
|
||||
node.outputs[0].type = inputType || "*";
|
||||
node.__outputType = displayType;
|
||||
node.outputs[0].type = inputType || '*'
|
||||
node.__outputType = displayType
|
||||
node.outputs[0].name = node.properties.showOutputText
|
||||
? displayType
|
||||
: "";
|
||||
node.size = node.computeSize();
|
||||
node.applyOrientation();
|
||||
: ''
|
||||
node.size = node.computeSize()
|
||||
node.applyOrientation()
|
||||
|
||||
for (const l of node.outputs[0].links || []) {
|
||||
const link = app.graph.links[l];
|
||||
const link = app.graph.links[l]
|
||||
if (link) {
|
||||
link.color = color;
|
||||
link.color = color
|
||||
|
||||
if (app.configuringGraph) continue;
|
||||
const targetNode = app.graph.getNodeById(link.target_id);
|
||||
const targetInput = targetNode.inputs?.[link.target_slot];
|
||||
if (app.configuringGraph) continue
|
||||
const targetNode = app.graph.getNodeById(link.target_id)
|
||||
const targetInput = targetNode.inputs?.[link.target_slot]
|
||||
if (targetInput?.widget) {
|
||||
const config = getWidgetConfig(targetInput);
|
||||
const config = getWidgetConfig(targetInput)
|
||||
if (!widgetConfig) {
|
||||
widgetConfig = config[1] ?? {};
|
||||
widgetType = config[0];
|
||||
widgetConfig = config[1] ?? {}
|
||||
widgetType = config[0]
|
||||
}
|
||||
if (!targetWidget) {
|
||||
targetWidget = targetNode.widgets?.find(
|
||||
(w) => w.name === targetInput.widget.name
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const merged = mergeIfValid(targetInput, [
|
||||
config[0],
|
||||
widgetConfig,
|
||||
]);
|
||||
widgetConfig
|
||||
])
|
||||
if (merged.customConfig) {
|
||||
widgetConfig = merged.customConfig;
|
||||
widgetConfig = merged.customConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,64 +194,64 @@ app.registerExtension({
|
||||
|
||||
for (const node of updateNodes) {
|
||||
if (widgetConfig && outputType) {
|
||||
node.inputs[0].widget = { name: "value" };
|
||||
node.inputs[0].widget = { name: 'value' }
|
||||
setWidgetConfig(
|
||||
node.inputs[0],
|
||||
[widgetType ?? displayType, widgetConfig],
|
||||
targetWidget
|
||||
);
|
||||
)
|
||||
} else {
|
||||
setWidgetConfig(node.inputs[0], null);
|
||||
setWidgetConfig(node.inputs[0], null)
|
||||
}
|
||||
}
|
||||
|
||||
if (inputNode) {
|
||||
const link = app.graph.links[inputNode.inputs[0].link];
|
||||
const link = app.graph.links[inputNode.inputs[0].link]
|
||||
if (link) {
|
||||
link.color = color;
|
||||
link.color = color
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.clone = function () {
|
||||
const cloned = RerouteNode.prototype.clone.apply(this);
|
||||
cloned.removeOutput(0);
|
||||
cloned.addOutput(this.properties.showOutputText ? "*" : "", "*");
|
||||
cloned.size = cloned.computeSize();
|
||||
return cloned;
|
||||
};
|
||||
const cloned = RerouteNode.prototype.clone.apply(this)
|
||||
cloned.removeOutput(0)
|
||||
cloned.addOutput(this.properties.showOutputText ? '*' : '', '*')
|
||||
cloned.size = cloned.computeSize()
|
||||
return cloned
|
||||
}
|
||||
|
||||
// This node is purely frontend and does not impact the resulting prompt so should not be serialized
|
||||
this.isVirtualNode = true;
|
||||
this.isVirtualNode = true
|
||||
}
|
||||
|
||||
getExtraMenuOptions(_, options) {
|
||||
options.unshift(
|
||||
{
|
||||
content:
|
||||
(this.properties.showOutputText ? "Hide" : "Show") + " Type",
|
||||
(this.properties.showOutputText ? 'Hide' : 'Show') + ' Type',
|
||||
callback: () => {
|
||||
this.properties.showOutputText = !this.properties.showOutputText;
|
||||
this.properties.showOutputText = !this.properties.showOutputText
|
||||
if (this.properties.showOutputText) {
|
||||
this.outputs[0].name =
|
||||
this.__outputType || (this.outputs[0].type as string);
|
||||
this.__outputType || (this.outputs[0].type as string)
|
||||
} else {
|
||||
this.outputs[0].name = "";
|
||||
this.outputs[0].name = ''
|
||||
}
|
||||
this.size = this.computeSize()
|
||||
this.applyOrientation()
|
||||
app.graph.setDirtyCanvas(true, true)
|
||||
}
|
||||
this.size = this.computeSize();
|
||||
this.applyOrientation();
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
},
|
||||
},
|
||||
{
|
||||
content:
|
||||
(RerouteNode.defaultVisibility ? "Hide" : "Show") +
|
||||
" Type By Default",
|
||||
(RerouteNode.defaultVisibility ? 'Hide' : 'Show') +
|
||||
' Type By Default',
|
||||
callback: () => {
|
||||
RerouteNode.setDefaultTextVisibility(
|
||||
!RerouteNode.defaultVisibility
|
||||
);
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
// naming is inverted with respect to LiteGraphNode.horizontal
|
||||
@@ -259,25 +259,25 @@ app.registerExtension({
|
||||
// each slot in the inputs and outputs are layed out horizontally,
|
||||
// which is the opposite of the visual orientation of the inputs and outputs as a node
|
||||
content:
|
||||
"Set " + (this.properties.horizontal ? "Horizontal" : "Vertical"),
|
||||
'Set ' + (this.properties.horizontal ? 'Horizontal' : 'Vertical'),
|
||||
callback: () => {
|
||||
this.properties.horizontal = !this.properties.horizontal;
|
||||
this.applyOrientation();
|
||||
},
|
||||
this.properties.horizontal = !this.properties.horizontal
|
||||
this.applyOrientation()
|
||||
}
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
applyOrientation() {
|
||||
this.horizontal = this.properties.horizontal;
|
||||
this.horizontal = this.properties.horizontal
|
||||
if (this.horizontal) {
|
||||
// we correct the input position, because LiteGraphNode.horizontal
|
||||
// doesn't account for title presence
|
||||
// which reroute nodes don't have
|
||||
this.inputs[0].pos = [this.size[0] / 2, 0];
|
||||
this.inputs[0].pos = [this.size[0] / 2, 0]
|
||||
} else {
|
||||
delete this.inputs[0].pos;
|
||||
delete this.inputs[0].pos
|
||||
}
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
app.graph.setDirtyCanvas(true, true)
|
||||
}
|
||||
|
||||
computeSize(): [number, number] {
|
||||
@@ -289,34 +289,34 @@ app.registerExtension({
|
||||
40
|
||||
)
|
||||
: 75,
|
||||
26,
|
||||
];
|
||||
26
|
||||
]
|
||||
}
|
||||
|
||||
static setDefaultTextVisibility(visible) {
|
||||
RerouteNode.defaultVisibility = visible;
|
||||
RerouteNode.defaultVisibility = visible
|
||||
if (visible) {
|
||||
localStorage["Comfy.RerouteNode.DefaultVisibility"] = "true";
|
||||
localStorage['Comfy.RerouteNode.DefaultVisibility'] = 'true'
|
||||
} else {
|
||||
delete localStorage["Comfy.RerouteNode.DefaultVisibility"];
|
||||
delete localStorage['Comfy.RerouteNode.DefaultVisibility']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load default visibility
|
||||
RerouteNode.setDefaultTextVisibility(
|
||||
!!localStorage["Comfy.RerouteNode.DefaultVisibility"]
|
||||
);
|
||||
!!localStorage['Comfy.RerouteNode.DefaultVisibility']
|
||||
)
|
||||
|
||||
LiteGraph.registerNodeType(
|
||||
"Reroute",
|
||||
'Reroute',
|
||||
Object.assign(RerouteNode, {
|
||||
title_mode: LiteGraph.NO_TITLE,
|
||||
title: "Reroute",
|
||||
collapsable: false,
|
||||
title: 'Reroute',
|
||||
collapsable: false
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
RerouteNode.category = "utils";
|
||||
},
|
||||
});
|
||||
RerouteNode.category = 'utils'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,43 +1,39 @@
|
||||
import { app } from "../../scripts/app";
|
||||
import { applyTextReplacements } from "../../scripts/utils";
|
||||
import { app } from '../../scripts/app'
|
||||
import { applyTextReplacements } from '../../scripts/utils'
|
||||
// Use widget values and dates in output filenames
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.SaveImageExtraOutput",
|
||||
name: 'Comfy.SaveImageExtraOutput',
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "SaveImage") {
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
if (nodeData.name === 'SaveImage') {
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated
|
||||
// When the SaveImage node is created we want to override the serialization of the output name widget to run our S&R
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
const r = onNodeCreated
|
||||
? onNodeCreated.apply(this, arguments)
|
||||
: undefined;
|
||||
: undefined
|
||||
|
||||
const widget = this.widgets.find((w) => w.name === "filename_prefix");
|
||||
const widget = this.widgets.find((w) => w.name === 'filename_prefix')
|
||||
widget.serializeValue = () => {
|
||||
return applyTextReplacements(app, widget.value);
|
||||
};
|
||||
return applyTextReplacements(app, widget.value)
|
||||
}
|
||||
|
||||
return r;
|
||||
};
|
||||
return r
|
||||
}
|
||||
} else {
|
||||
// When any other node is created add a property to alias the node
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
const r = onNodeCreated
|
||||
? onNodeCreated.apply(this, arguments)
|
||||
: undefined;
|
||||
: undefined
|
||||
|
||||
if (!this.properties || !("Node name for S&R" in this.properties)) {
|
||||
this.addProperty(
|
||||
"Node name for S&R",
|
||||
this.constructor.type,
|
||||
"string"
|
||||
);
|
||||
if (!this.properties || !('Node name for S&R' in this.properties)) {
|
||||
this.addProperty('Node name for S&R', this.constructor.type, 'string')
|
||||
}
|
||||
|
||||
return r;
|
||||
};
|
||||
return r
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,115 +1,115 @@
|
||||
import { app } from "../../scripts/app";
|
||||
import { LGraphCanvas, LiteGraph } from "@comfyorg/litegraph";
|
||||
import { app } from '../../scripts/app'
|
||||
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
||||
|
||||
let touchZooming;
|
||||
let touchCount = 0;
|
||||
let touchZooming
|
||||
let touchCount = 0
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.SimpleTouchSupport",
|
||||
name: 'Comfy.SimpleTouchSupport',
|
||||
setup() {
|
||||
let zoomPos;
|
||||
let touchTime;
|
||||
let lastTouch;
|
||||
let zoomPos
|
||||
let touchTime
|
||||
let lastTouch
|
||||
|
||||
function getMultiTouchPos(e) {
|
||||
return Math.hypot(
|
||||
e.touches[0].clientX - e.touches[1].clientX,
|
||||
e.touches[0].clientY - e.touches[1].clientY
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
app.canvasEl.addEventListener(
|
||||
"touchstart",
|
||||
'touchstart',
|
||||
(e) => {
|
||||
touchCount++;
|
||||
lastTouch = null;
|
||||
touchCount++
|
||||
lastTouch = null
|
||||
if (e.touches?.length === 1) {
|
||||
// Store start time for press+hold for context menu
|
||||
touchTime = new Date();
|
||||
lastTouch = e.touches[0];
|
||||
touchTime = new Date()
|
||||
lastTouch = e.touches[0]
|
||||
} else {
|
||||
touchTime = null;
|
||||
touchTime = null
|
||||
if (e.touches?.length === 2) {
|
||||
// Store center pos for zoom
|
||||
zoomPos = getMultiTouchPos(e);
|
||||
app.canvas.pointer_is_down = false;
|
||||
zoomPos = getMultiTouchPos(e)
|
||||
app.canvas.pointer_is_down = false
|
||||
}
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
)
|
||||
|
||||
app.canvasEl.addEventListener("touchend", (e: TouchEvent) => {
|
||||
touchZooming = false;
|
||||
touchCount = e.touches?.length ?? touchCount - 1;
|
||||
app.canvasEl.addEventListener('touchend', (e: TouchEvent) => {
|
||||
touchZooming = false
|
||||
touchCount = e.touches?.length ?? touchCount - 1
|
||||
if (touchTime && !e.touches?.length) {
|
||||
if (new Date().getTime() - touchTime > 600) {
|
||||
try {
|
||||
// hack to get litegraph to use this event
|
||||
e.constructor = CustomEvent;
|
||||
e.constructor = CustomEvent
|
||||
} catch (error) {}
|
||||
// @ts-ignore
|
||||
e.clientX = lastTouch.clientX;
|
||||
e.clientX = lastTouch.clientX
|
||||
// @ts-ignore
|
||||
e.clientY = lastTouch.clientY;
|
||||
e.clientY = lastTouch.clientY
|
||||
|
||||
app.canvas.pointer_is_down = true;
|
||||
app.canvas.pointer_is_down = true
|
||||
// @ts-ignore
|
||||
app.canvas._mousedown_callback(e);
|
||||
app.canvas._mousedown_callback(e)
|
||||
}
|
||||
touchTime = null;
|
||||
touchTime = null
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
app.canvasEl.addEventListener(
|
||||
"touchmove",
|
||||
'touchmove',
|
||||
(e) => {
|
||||
touchTime = null;
|
||||
touchTime = null
|
||||
if (e.touches?.length === 2) {
|
||||
app.canvas.pointer_is_down = false;
|
||||
touchZooming = true;
|
||||
app.canvas.pointer_is_down = false
|
||||
touchZooming = true
|
||||
// @ts-ignore
|
||||
LiteGraph.closeAllContextMenus();
|
||||
LiteGraph.closeAllContextMenus()
|
||||
// @ts-ignore
|
||||
app.canvas.search_box?.close();
|
||||
const newZoomPos = getMultiTouchPos(e);
|
||||
app.canvas.search_box?.close()
|
||||
const newZoomPos = getMultiTouchPos(e)
|
||||
|
||||
const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
|
||||
const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
|
||||
const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2
|
||||
const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2
|
||||
|
||||
let scale = app.canvas.ds.scale;
|
||||
const diff = zoomPos - newZoomPos;
|
||||
let scale = app.canvas.ds.scale
|
||||
const diff = zoomPos - newZoomPos
|
||||
if (diff > 0.5) {
|
||||
scale *= 1 / 1.07;
|
||||
scale *= 1 / 1.07
|
||||
} else if (diff < -0.5) {
|
||||
scale *= 1.07;
|
||||
scale *= 1.07
|
||||
}
|
||||
app.canvas.ds.changeScale(scale, [midX, midY]);
|
||||
app.canvas.setDirty(true, true);
|
||||
zoomPos = newZoomPos;
|
||||
app.canvas.ds.changeScale(scale, [midX, midY])
|
||||
app.canvas.setDirty(true, true)
|
||||
zoomPos = newZoomPos
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
},
|
||||
});
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// @ts-ignore
|
||||
const processMouseDown = LGraphCanvas.prototype.processMouseDown;
|
||||
const processMouseDown = LGraphCanvas.prototype.processMouseDown
|
||||
// @ts-ignore
|
||||
LGraphCanvas.prototype.processMouseDown = function (e) {
|
||||
if (touchZooming || touchCount) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
return processMouseDown.apply(this, arguments);
|
||||
};
|
||||
return processMouseDown.apply(this, arguments)
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const processMouseMove = LGraphCanvas.prototype.processMouseMove;
|
||||
const processMouseMove = LGraphCanvas.prototype.processMouseMove
|
||||
// @ts-ignore
|
||||
LGraphCanvas.prototype.processMouseMove = function (e) {
|
||||
if (touchZooming || touchCount > 1) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
return processMouseMove.apply(this, arguments);
|
||||
};
|
||||
return processMouseMove.apply(this, arguments)
|
||||
}
|
||||
|
||||
@@ -1,97 +1,97 @@
|
||||
import { app } from "../../scripts/app";
|
||||
import { ComfyWidgets } from "../../scripts/widgets";
|
||||
import { LiteGraph } from "@comfyorg/litegraph";
|
||||
import { app } from '../../scripts/app'
|
||||
import { ComfyWidgets } from '../../scripts/widgets'
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
// Adds defaults for quickly adding nodes with middle click on the input/output
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.SlotDefaults",
|
||||
name: 'Comfy.SlotDefaults',
|
||||
suggestionsNumber: null,
|
||||
init() {
|
||||
LiteGraph.search_filter_enabled = true;
|
||||
LiteGraph.middle_click_slot_add_default_node = true;
|
||||
LiteGraph.search_filter_enabled = true
|
||||
LiteGraph.middle_click_slot_add_default_node = true
|
||||
this.suggestionsNumber = app.ui.settings.addSetting({
|
||||
id: "Comfy.NodeSuggestions.number",
|
||||
name: "Number of nodes suggestions",
|
||||
type: "slider",
|
||||
id: 'Comfy.NodeSuggestions.number',
|
||||
name: 'Number of nodes suggestions',
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 1,
|
||||
max: 100,
|
||||
step: 1,
|
||||
step: 1
|
||||
},
|
||||
defaultValue: 5,
|
||||
onChange: (newVal, oldVal) => {
|
||||
this.setDefaults(newVal);
|
||||
},
|
||||
});
|
||||
this.setDefaults(newVal)
|
||||
}
|
||||
})
|
||||
},
|
||||
slot_types_default_out: {},
|
||||
slot_types_default_in: {},
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
var nodeId = nodeData.name;
|
||||
var inputs = [];
|
||||
inputs = nodeData["input"]["required"]; //only show required inputs to reduce the mess also not logical to create node with optional inputs
|
||||
var nodeId = nodeData.name
|
||||
var inputs = []
|
||||
inputs = nodeData['input']['required'] //only show required inputs to reduce the mess also not logical to create node with optional inputs
|
||||
for (const inputKey in inputs) {
|
||||
var input = inputs[inputKey];
|
||||
if (typeof input[0] !== "string") continue;
|
||||
var input = inputs[inputKey]
|
||||
if (typeof input[0] !== 'string') continue
|
||||
|
||||
var type = input[0];
|
||||
var type = input[0]
|
||||
if (type in ComfyWidgets) {
|
||||
var customProperties = input[1];
|
||||
if (!customProperties?.forceInput) continue; //ignore widgets that don't force input
|
||||
var customProperties = input[1]
|
||||
if (!customProperties?.forceInput) continue //ignore widgets that don't force input
|
||||
}
|
||||
|
||||
if (!(type in this.slot_types_default_out)) {
|
||||
this.slot_types_default_out[type] = ["Reroute"];
|
||||
this.slot_types_default_out[type] = ['Reroute']
|
||||
}
|
||||
if (this.slot_types_default_out[type].includes(nodeId)) continue;
|
||||
this.slot_types_default_out[type].push(nodeId);
|
||||
if (this.slot_types_default_out[type].includes(nodeId)) continue
|
||||
this.slot_types_default_out[type].push(nodeId)
|
||||
|
||||
// Input types have to be stored as lower case
|
||||
// Store each node that can handle this input type
|
||||
const lowerType = type.toLocaleLowerCase();
|
||||
const lowerType = type.toLocaleLowerCase()
|
||||
if (!(lowerType in LiteGraph.registered_slot_in_types)) {
|
||||
LiteGraph.registered_slot_in_types[lowerType] = { nodes: [] };
|
||||
LiteGraph.registered_slot_in_types[lowerType] = { nodes: [] }
|
||||
}
|
||||
LiteGraph.registered_slot_in_types[lowerType].nodes.push(
|
||||
nodeType.comfyClass
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
var outputs = nodeData["output"];
|
||||
var outputs = nodeData['output']
|
||||
for (const key in outputs) {
|
||||
var type = outputs[key] as string;
|
||||
var type = outputs[key] as string
|
||||
if (!(type in this.slot_types_default_in)) {
|
||||
this.slot_types_default_in[type] = ["Reroute"]; // ["Reroute", "Primitive"]; primitive doesn't always work :'()
|
||||
this.slot_types_default_in[type] = ['Reroute'] // ["Reroute", "Primitive"]; primitive doesn't always work :'()
|
||||
}
|
||||
|
||||
this.slot_types_default_in[type].push(nodeId);
|
||||
this.slot_types_default_in[type].push(nodeId)
|
||||
|
||||
// Store each node that can handle this output type
|
||||
if (!(type in LiteGraph.registered_slot_out_types)) {
|
||||
LiteGraph.registered_slot_out_types[type] = { nodes: [] };
|
||||
LiteGraph.registered_slot_out_types[type] = { nodes: [] }
|
||||
}
|
||||
LiteGraph.registered_slot_out_types[type].nodes.push(nodeType.comfyClass);
|
||||
LiteGraph.registered_slot_out_types[type].nodes.push(nodeType.comfyClass)
|
||||
|
||||
if (!LiteGraph.slot_types_out.includes(type)) {
|
||||
LiteGraph.slot_types_out.push(type);
|
||||
LiteGraph.slot_types_out.push(type)
|
||||
}
|
||||
}
|
||||
var maxNum = this.suggestionsNumber.value;
|
||||
this.setDefaults(maxNum);
|
||||
var maxNum = this.suggestionsNumber.value
|
||||
this.setDefaults(maxNum)
|
||||
},
|
||||
setDefaults(maxNum) {
|
||||
LiteGraph.slot_types_default_out = {};
|
||||
LiteGraph.slot_types_default_in = {};
|
||||
LiteGraph.slot_types_default_out = {}
|
||||
LiteGraph.slot_types_default_in = {}
|
||||
|
||||
for (const type in this.slot_types_default_out) {
|
||||
LiteGraph.slot_types_default_out[type] = this.slot_types_default_out[
|
||||
type
|
||||
].slice(0, maxNum);
|
||||
].slice(0, maxNum)
|
||||
}
|
||||
for (const type in this.slot_types_default_in) {
|
||||
LiteGraph.slot_types_default_in[type] = this.slot_types_default_in[
|
||||
type
|
||||
].slice(0, maxNum);
|
||||
].slice(0, maxNum)
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,75 +1,73 @@
|
||||
import { app } from "../../scripts/app";
|
||||
import { app } from '../../scripts/app'
|
||||
import {
|
||||
LGraphCanvas,
|
||||
LGraphNode,
|
||||
LGraphGroup,
|
||||
LiteGraph,
|
||||
} from "@comfyorg/litegraph";
|
||||
LiteGraph
|
||||
} from '@comfyorg/litegraph'
|
||||
|
||||
// Shift + drag/resize to snap to grid
|
||||
|
||||
/** Rounds a Vector2 in-place to the current CANVAS_GRID_SIZE. */
|
||||
function roundVectorToGrid(vec) {
|
||||
vec[0] =
|
||||
LiteGraph.CANVAS_GRID_SIZE *
|
||||
Math.round(vec[0] / LiteGraph.CANVAS_GRID_SIZE);
|
||||
LiteGraph.CANVAS_GRID_SIZE * Math.round(vec[0] / LiteGraph.CANVAS_GRID_SIZE)
|
||||
vec[1] =
|
||||
LiteGraph.CANVAS_GRID_SIZE *
|
||||
Math.round(vec[1] / LiteGraph.CANVAS_GRID_SIZE);
|
||||
return vec;
|
||||
LiteGraph.CANVAS_GRID_SIZE * Math.round(vec[1] / LiteGraph.CANVAS_GRID_SIZE)
|
||||
return vec
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.SnapToGrid",
|
||||
name: 'Comfy.SnapToGrid',
|
||||
init() {
|
||||
// Add setting to control grid size
|
||||
app.ui.settings.addSetting({
|
||||
id: "Comfy.SnapToGrid.GridSize",
|
||||
name: "Grid Size",
|
||||
type: "slider",
|
||||
id: 'Comfy.SnapToGrid.GridSize',
|
||||
name: 'Grid Size',
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 1,
|
||||
max: 500,
|
||||
max: 500
|
||||
},
|
||||
tooltip:
|
||||
"When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid.",
|
||||
'When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid.',
|
||||
defaultValue: LiteGraph.CANVAS_GRID_SIZE,
|
||||
onChange(value) {
|
||||
LiteGraph.CANVAS_GRID_SIZE = +value;
|
||||
},
|
||||
});
|
||||
LiteGraph.CANVAS_GRID_SIZE = +value
|
||||
}
|
||||
})
|
||||
|
||||
// After moving a node, if the shift key is down align it to grid
|
||||
const onNodeMoved = app.canvas.onNodeMoved;
|
||||
const onNodeMoved = app.canvas.onNodeMoved
|
||||
app.canvas.onNodeMoved = function (node) {
|
||||
const r = onNodeMoved?.apply(this, arguments);
|
||||
const r = onNodeMoved?.apply(this, arguments)
|
||||
|
||||
if (app.shiftDown) {
|
||||
// Ensure all selected nodes are realigned
|
||||
for (const id in this.selected_nodes) {
|
||||
this.selected_nodes[id].alignToGrid();
|
||||
this.selected_nodes[id].alignToGrid()
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
};
|
||||
return r
|
||||
}
|
||||
|
||||
// When a node is added, add a resize handler to it so we can fix align the size with the grid
|
||||
const onNodeAdded = app.graph.onNodeAdded;
|
||||
const onNodeAdded = app.graph.onNodeAdded
|
||||
app.graph.onNodeAdded = function (node) {
|
||||
const onResize = node.onResize;
|
||||
const onResize = node.onResize
|
||||
node.onResize = function () {
|
||||
if (app.shiftDown) {
|
||||
roundVectorToGrid(node.size);
|
||||
roundVectorToGrid(node.size)
|
||||
}
|
||||
return onResize?.apply(this, arguments)
|
||||
}
|
||||
return onNodeAdded?.apply(this, arguments)
|
||||
}
|
||||
return onResize?.apply(this, arguments);
|
||||
};
|
||||
return onNodeAdded?.apply(this, arguments);
|
||||
};
|
||||
|
||||
// Draw a preview of where the node will go if holding shift and the node is selected
|
||||
// @ts-ignore
|
||||
const origDrawNode = LGraphCanvas.prototype.drawNode;
|
||||
const origDrawNode = LGraphCanvas.prototype.drawNode
|
||||
// @ts-ignore
|
||||
LGraphCanvas.prototype.drawNode = function (node, ctx) {
|
||||
if (
|
||||
@@ -77,53 +75,53 @@ app.registerExtension({
|
||||
this.node_dragged &&
|
||||
node.id in this.selected_nodes
|
||||
) {
|
||||
const [x, y] = roundVectorToGrid([...node.pos]);
|
||||
const shiftX = x - node.pos[0];
|
||||
let shiftY = y - node.pos[1];
|
||||
const [x, y] = roundVectorToGrid([...node.pos])
|
||||
const shiftX = x - node.pos[0]
|
||||
let shiftY = y - node.pos[1]
|
||||
|
||||
let w, h;
|
||||
let w, h
|
||||
if (node.flags.collapsed) {
|
||||
// @ts-ignore
|
||||
w = node._collapsed_width;
|
||||
h = LiteGraph.NODE_TITLE_HEIGHT;
|
||||
shiftY -= LiteGraph.NODE_TITLE_HEIGHT;
|
||||
w = node._collapsed_width
|
||||
h = LiteGraph.NODE_TITLE_HEIGHT
|
||||
shiftY -= LiteGraph.NODE_TITLE_HEIGHT
|
||||
} else {
|
||||
w = node.size[0];
|
||||
h = node.size[1];
|
||||
w = node.size[0]
|
||||
h = node.size[1]
|
||||
// @ts-ignore
|
||||
let titleMode = node.constructor.title_mode;
|
||||
let titleMode = node.constructor.title_mode
|
||||
if (
|
||||
titleMode !== LiteGraph.TRANSPARENT_TITLE &&
|
||||
titleMode !== LiteGraph.NO_TITLE
|
||||
) {
|
||||
h += LiteGraph.NODE_TITLE_HEIGHT;
|
||||
shiftY -= LiteGraph.NODE_TITLE_HEIGHT;
|
||||
h += LiteGraph.NODE_TITLE_HEIGHT
|
||||
shiftY -= LiteGraph.NODE_TITLE_HEIGHT
|
||||
}
|
||||
}
|
||||
const f = ctx.fillStyle;
|
||||
ctx.fillStyle = "rgba(100, 100, 100, 0.5)";
|
||||
ctx.fillRect(shiftX, shiftY, w, h);
|
||||
ctx.fillStyle = f;
|
||||
const f = ctx.fillStyle
|
||||
ctx.fillStyle = 'rgba(100, 100, 100, 0.5)'
|
||||
ctx.fillRect(shiftX, shiftY, w, h)
|
||||
ctx.fillStyle = f
|
||||
}
|
||||
|
||||
return origDrawNode.apply(this, arguments);
|
||||
};
|
||||
return origDrawNode.apply(this, arguments)
|
||||
}
|
||||
|
||||
/**
|
||||
* The currently moving, selected group only. Set after the `selected_group` has actually started
|
||||
* moving.
|
||||
*/
|
||||
let selectedAndMovingGroup: LGraphGroup | null = null;
|
||||
let selectedAndMovingGroup: LGraphGroup | null = null
|
||||
|
||||
/**
|
||||
* Handles moving a group; tracking when a group has been moved (to show the ghost in `drawGroups`
|
||||
* below) as well as handle the last move call from LiteGraph's `processMouseUp`.
|
||||
*/
|
||||
// @ts-ignore
|
||||
const groupMove = LGraphGroup.prototype.move;
|
||||
const groupMove = LGraphGroup.prototype.move
|
||||
// @ts-ignore
|
||||
LGraphGroup.prototype.move = function (deltax, deltay, ignore_nodes) {
|
||||
const v = groupMove.apply(this, arguments);
|
||||
const v = groupMove.apply(this, arguments)
|
||||
// When we've started moving, set `selectedAndMovingGroup` as LiteGraph sets `selected_group`
|
||||
// too eagerly and we don't want to behave like we're moving until we get a delta.
|
||||
if (
|
||||
@@ -131,7 +129,7 @@ app.registerExtension({
|
||||
app.canvas.selected_group === this &&
|
||||
(deltax || deltay)
|
||||
) {
|
||||
selectedAndMovingGroup = this;
|
||||
selectedAndMovingGroup = this
|
||||
}
|
||||
|
||||
// LiteGraph will call group.move both on mouse-move as well as mouse-up though we only want
|
||||
@@ -141,15 +139,15 @@ app.registerExtension({
|
||||
if (app.canvas.last_mouse_dragging === false && app.shiftDown) {
|
||||
// After moving a group (while app.shiftDown), snap all the child nodes and, finally,
|
||||
// align the group itself.
|
||||
this.recomputeInsideNodes();
|
||||
this.recomputeInsideNodes()
|
||||
for (const node of this._nodes) {
|
||||
node.alignToGrid();
|
||||
node.alignToGrid()
|
||||
}
|
||||
// @ts-ignore
|
||||
LGraphNode.prototype.alignToGrid.apply(this);
|
||||
LGraphNode.prototype.alignToGrid.apply(this)
|
||||
}
|
||||
return v
|
||||
}
|
||||
return v;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles drawing a group when, snapping the size when one is actively being resized tracking and/or
|
||||
@@ -157,50 +155,50 @@ app.registerExtension({
|
||||
* both.
|
||||
*/
|
||||
// @ts-ignore
|
||||
const drawGroups = LGraphCanvas.prototype.drawGroups;
|
||||
const drawGroups = LGraphCanvas.prototype.drawGroups
|
||||
// @ts-ignore
|
||||
LGraphCanvas.prototype.drawGroups = function (canvas, ctx) {
|
||||
if (this.selected_group && app.shiftDown) {
|
||||
if (this.selected_group_resizing) {
|
||||
// @ts-ignore
|
||||
roundVectorToGrid(this.selected_group.size);
|
||||
roundVectorToGrid(this.selected_group.size)
|
||||
} else if (selectedAndMovingGroup) {
|
||||
// @ts-ignore
|
||||
const [x, y] = roundVectorToGrid([...selectedAndMovingGroup.pos]);
|
||||
const f = ctx.fillStyle;
|
||||
const s = ctx.strokeStyle;
|
||||
ctx.fillStyle = "rgba(100, 100, 100, 0.33)";
|
||||
ctx.strokeStyle = "rgba(100, 100, 100, 0.66)";
|
||||
const [x, y] = roundVectorToGrid([...selectedAndMovingGroup.pos])
|
||||
const f = ctx.fillStyle
|
||||
const s = ctx.strokeStyle
|
||||
ctx.fillStyle = 'rgba(100, 100, 100, 0.33)'
|
||||
ctx.strokeStyle = 'rgba(100, 100, 100, 0.66)'
|
||||
// @ts-ignore
|
||||
ctx.rect(x, y, ...selectedAndMovingGroup.size);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = f;
|
||||
ctx.strokeStyle = s;
|
||||
ctx.rect(x, y, ...selectedAndMovingGroup.size)
|
||||
ctx.fill()
|
||||
ctx.stroke()
|
||||
ctx.fillStyle = f
|
||||
ctx.strokeStyle = s
|
||||
}
|
||||
} else if (!this.selected_group) {
|
||||
selectedAndMovingGroup = null;
|
||||
selectedAndMovingGroup = null
|
||||
}
|
||||
return drawGroups.apply(this, arguments)
|
||||
}
|
||||
return drawGroups.apply(this, arguments);
|
||||
};
|
||||
|
||||
/** Handles adding a group in a snapping-enabled state. */
|
||||
// @ts-ignore
|
||||
const onGroupAdd = LGraphCanvas.onGroupAdd;
|
||||
const onGroupAdd = LGraphCanvas.onGroupAdd
|
||||
// @ts-ignore
|
||||
LGraphCanvas.onGroupAdd = function () {
|
||||
const v = onGroupAdd.apply(app.canvas, arguments);
|
||||
const v = onGroupAdd.apply(app.canvas, arguments)
|
||||
if (app.shiftDown) {
|
||||
// @ts-ignore
|
||||
const lastGroup = app.graph._groups[app.graph._groups.length - 1];
|
||||
const lastGroup = app.graph._groups[app.graph._groups.length - 1]
|
||||
if (lastGroup) {
|
||||
// @ts-ignore
|
||||
roundVectorToGrid(lastGroup.pos);
|
||||
roundVectorToGrid(lastGroup.pos)
|
||||
// @ts-ignore
|
||||
roundVectorToGrid(lastGroup.size);
|
||||
roundVectorToGrid(lastGroup.size)
|
||||
}
|
||||
}
|
||||
return v;
|
||||
};
|
||||
},
|
||||
});
|
||||
return v
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
import { app } from "../../scripts/app";
|
||||
import { api } from "../../scripts/api";
|
||||
import type { IWidget } from "@comfyorg/litegraph";
|
||||
import type { DOMWidget } from "@/scripts/domWidget";
|
||||
import { ComfyNodeDef } from "@/types/apiTypes";
|
||||
import { app } from '../../scripts/app'
|
||||
import { api } from '../../scripts/api'
|
||||
import type { IWidget } from '@comfyorg/litegraph'
|
||||
import type { DOMWidget } from '@/scripts/domWidget'
|
||||
import { ComfyNodeDef } from '@/types/apiTypes'
|
||||
|
||||
type FolderType = "input" | "output" | "temp";
|
||||
type FolderType = 'input' | 'output' | 'temp'
|
||||
|
||||
function splitFilePath(path: string): [string, string] {
|
||||
const folder_separator = path.lastIndexOf("/");
|
||||
const folder_separator = path.lastIndexOf('/')
|
||||
if (folder_separator === -1) {
|
||||
return ["", path];
|
||||
return ['', path]
|
||||
}
|
||||
return [
|
||||
path.substring(0, folder_separator),
|
||||
path.substring(folder_separator + 1),
|
||||
];
|
||||
path.substring(folder_separator + 1)
|
||||
]
|
||||
}
|
||||
|
||||
function getResourceURL(
|
||||
subfolder: string,
|
||||
filename: string,
|
||||
type: FolderType = "input"
|
||||
type: FolderType = 'input'
|
||||
): string {
|
||||
const params = [
|
||||
"filename=" + encodeURIComponent(filename),
|
||||
"type=" + type,
|
||||
"subfolder=" + subfolder,
|
||||
app.getRandParam().substring(1),
|
||||
].join("&");
|
||||
'filename=' + encodeURIComponent(filename),
|
||||
'type=' + type,
|
||||
'subfolder=' + subfolder,
|
||||
app.getRandParam().substring(1)
|
||||
].join('&')
|
||||
|
||||
return `/view?${params}`;
|
||||
return `/view?${params}`
|
||||
}
|
||||
|
||||
async function uploadFile(
|
||||
@@ -41,109 +41,109 @@ async function uploadFile(
|
||||
) {
|
||||
try {
|
||||
// Wrap file in formdata so it includes filename
|
||||
const body = new FormData();
|
||||
body.append("image", file);
|
||||
if (pasted) body.append("subfolder", "pasted");
|
||||
const resp = await api.fetchApi("/upload/image", {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
const body = new FormData()
|
||||
body.append('image', file)
|
||||
if (pasted) body.append('subfolder', 'pasted')
|
||||
const resp = await api.fetchApi('/upload/image', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
|
||||
if (resp.status === 200) {
|
||||
const data = await resp.json();
|
||||
const data = await resp.json()
|
||||
// Add the file to the dropdown list and update the widget value
|
||||
let path = data.name;
|
||||
if (data.subfolder) path = data.subfolder + "/" + path;
|
||||
let path = data.name
|
||||
if (data.subfolder) path = data.subfolder + '/' + path
|
||||
|
||||
if (!audioWidget.options.values.includes(path)) {
|
||||
audioWidget.options.values.push(path);
|
||||
audioWidget.options.values.push(path)
|
||||
}
|
||||
|
||||
if (updateNode) {
|
||||
audioUIWidget.element.src = api.apiURL(
|
||||
getResourceURL(...splitFilePath(path))
|
||||
);
|
||||
audioWidget.value = path;
|
||||
)
|
||||
audioWidget.value = path
|
||||
}
|
||||
} else {
|
||||
alert(resp.status + " - " + resp.statusText);
|
||||
alert(resp.status + ' - ' + resp.statusText)
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error);
|
||||
alert(error)
|
||||
}
|
||||
}
|
||||
|
||||
// AudioWidget MUST be registered first, as AUDIOUPLOAD depends on AUDIO_UI to be
|
||||
// present.
|
||||
app.registerExtension({
|
||||
name: "Comfy.AudioWidget",
|
||||
name: 'Comfy.AudioWidget',
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (
|
||||
["LoadAudio", "SaveAudio", "PreviewAudio"].includes(nodeType.comfyClass)
|
||||
['LoadAudio', 'SaveAudio', 'PreviewAudio'].includes(nodeType.comfyClass)
|
||||
) {
|
||||
nodeData.input.required.audioUI = ["AUDIO_UI"];
|
||||
nodeData.input.required.audioUI = ['AUDIO_UI']
|
||||
}
|
||||
},
|
||||
getCustomWidgets() {
|
||||
return {
|
||||
AUDIO_UI(node, inputName: string) {
|
||||
const audio = document.createElement("audio");
|
||||
audio.controls = true;
|
||||
audio.classList.add("comfy-audio");
|
||||
audio.setAttribute("name", "media");
|
||||
const audio = document.createElement('audio')
|
||||
audio.controls = true
|
||||
audio.classList.add('comfy-audio')
|
||||
audio.setAttribute('name', 'media')
|
||||
|
||||
const audioUIWidget: DOMWidget<HTMLAudioElement> = node.addDOMWidget(
|
||||
inputName,
|
||||
/* name=*/ "audioUI",
|
||||
/* name=*/ 'audioUI',
|
||||
audio
|
||||
);
|
||||
)
|
||||
// @ts-ignore
|
||||
// TODO: Sort out the DOMWidget type.
|
||||
audioUIWidget.serialize = false;
|
||||
audioUIWidget.serialize = false
|
||||
|
||||
const isOutputNode = node.constructor.nodeData.output_node;
|
||||
const isOutputNode = node.constructor.nodeData.output_node
|
||||
if (isOutputNode) {
|
||||
// Hide the audio widget when there is no audio initially.
|
||||
audioUIWidget.element.classList.add("empty-audio-widget");
|
||||
audioUIWidget.element.classList.add('empty-audio-widget')
|
||||
// Populate the audio widget UI on node execution.
|
||||
const onExecuted = node.onExecuted;
|
||||
const onExecuted = node.onExecuted
|
||||
node.onExecuted = function (message) {
|
||||
onExecuted?.apply(this, arguments);
|
||||
const audios = message.audio;
|
||||
if (!audios) return;
|
||||
const audio = audios[0];
|
||||
onExecuted?.apply(this, arguments)
|
||||
const audios = message.audio
|
||||
if (!audios) return
|
||||
const audio = audios[0]
|
||||
audioUIWidget.element.src = api.apiURL(
|
||||
getResourceURL(audio.subfolder, audio.filename, audio.type)
|
||||
);
|
||||
audioUIWidget.element.classList.remove("empty-audio-widget");
|
||||
};
|
||||
)
|
||||
audioUIWidget.element.classList.remove('empty-audio-widget')
|
||||
}
|
||||
}
|
||||
return { widget: audioUIWidget }
|
||||
}
|
||||
}
|
||||
return { widget: audioUIWidget };
|
||||
},
|
||||
};
|
||||
},
|
||||
onNodeOutputsUpdated(nodeOutputs: Record<number, any>) {
|
||||
for (const [nodeId, output] of Object.entries(nodeOutputs)) {
|
||||
const node = app.graph.getNodeById(Number.parseInt(nodeId));
|
||||
if ("audio" in output) {
|
||||
const node = app.graph.getNodeById(Number.parseInt(nodeId))
|
||||
if ('audio' in output) {
|
||||
const audioUIWidget = node.widgets.find(
|
||||
(w) => w.name === "audioUI"
|
||||
) as unknown as DOMWidget<HTMLAudioElement>;
|
||||
const audio = output.audio[0];
|
||||
(w) => w.name === 'audioUI'
|
||||
) as unknown as DOMWidget<HTMLAudioElement>
|
||||
const audio = output.audio[0]
|
||||
audioUIWidget.element.src = api.apiURL(
|
||||
getResourceURL(audio.subfolder, audio.filename, audio.type)
|
||||
);
|
||||
audioUIWidget.element.classList.remove("empty-audio-widget");
|
||||
)
|
||||
audioUIWidget.element.classList.remove('empty-audio-widget')
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.UploadAudio",
|
||||
name: 'Comfy.UploadAudio',
|
||||
async beforeRegisterNodeDef(nodeType, nodeData: ComfyNodeDef) {
|
||||
if (nodeData?.input?.required?.audio?.[1]?.audio_upload === true) {
|
||||
nodeData.input.required.upload = ["AUDIOUPLOAD"];
|
||||
nodeData.input.required.upload = ['AUDIOUPLOAD']
|
||||
}
|
||||
},
|
||||
getCustomWidgets() {
|
||||
@@ -151,55 +151,55 @@ app.registerExtension({
|
||||
AUDIOUPLOAD(node, inputName: string) {
|
||||
// The widget that allows user to select file.
|
||||
const audioWidget: IWidget = node.widgets.find(
|
||||
(w: IWidget) => w.name === "audio"
|
||||
);
|
||||
(w: IWidget) => w.name === 'audio'
|
||||
)
|
||||
const audioUIWidget: DOMWidget<HTMLAudioElement> = node.widgets.find(
|
||||
(w: IWidget) => w.name === "audioUI"
|
||||
);
|
||||
(w: IWidget) => w.name === 'audioUI'
|
||||
)
|
||||
|
||||
const onAudioWidgetUpdate = () => {
|
||||
audioUIWidget.element.src = api.apiURL(
|
||||
getResourceURL(...splitFilePath(audioWidget.value))
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
// Initially load default audio file to audioUIWidget.
|
||||
if (audioWidget.value) {
|
||||
onAudioWidgetUpdate();
|
||||
onAudioWidgetUpdate()
|
||||
}
|
||||
audioWidget.callback = onAudioWidgetUpdate;
|
||||
audioWidget.callback = onAudioWidgetUpdate
|
||||
|
||||
// Load saved audio file widget values if restoring from workflow
|
||||
const onGraphConfigured = node.onGraphConfigured;
|
||||
const onGraphConfigured = node.onGraphConfigured
|
||||
node.onGraphConfigured = function () {
|
||||
onGraphConfigured?.apply(this, arguments);
|
||||
onGraphConfigured?.apply(this, arguments)
|
||||
if (audioWidget.value) {
|
||||
onAudioWidgetUpdate();
|
||||
onAudioWidgetUpdate()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fileInput = document.createElement("input");
|
||||
fileInput.type = "file";
|
||||
fileInput.accept = "audio/*";
|
||||
fileInput.style.display = "none";
|
||||
const fileInput = document.createElement('input')
|
||||
fileInput.type = 'file'
|
||||
fileInput.accept = 'audio/*'
|
||||
fileInput.style.display = 'none'
|
||||
fileInput.onchange = () => {
|
||||
if (fileInput.files.length) {
|
||||
uploadFile(audioWidget, audioUIWidget, fileInput.files[0], true);
|
||||
uploadFile(audioWidget, audioUIWidget, fileInput.files[0], true)
|
||||
}
|
||||
}
|
||||
};
|
||||
// The widget to pop up the upload dialog.
|
||||
const uploadWidget = node.addWidget(
|
||||
"button",
|
||||
'button',
|
||||
inputName,
|
||||
/* value=*/ "",
|
||||
/* value=*/ '',
|
||||
() => {
|
||||
fileInput.click();
|
||||
fileInput.click()
|
||||
}
|
||||
);
|
||||
uploadWidget.label = "choose file to upload";
|
||||
uploadWidget.serialize = false;
|
||||
)
|
||||
uploadWidget.label = 'choose file to upload'
|
||||
uploadWidget.serialize = false
|
||||
|
||||
return { widget: uploadWidget };
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
return { widget: uploadWidget }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { app } from "../../scripts/app";
|
||||
import { ComfyNodeDef } from "@/types/apiTypes";
|
||||
import { app } from '../../scripts/app'
|
||||
import { ComfyNodeDef } from '@/types/apiTypes'
|
||||
|
||||
// Adds an upload button to the nodes
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.UploadImage",
|
||||
name: 'Comfy.UploadImage',
|
||||
async beforeRegisterNodeDef(nodeType, nodeData: ComfyNodeDef, app) {
|
||||
if (nodeData?.input?.required?.image?.[1]?.image_upload === true) {
|
||||
nodeData.input.required.upload = ["IMAGEUPLOAD"];
|
||||
nodeData.input.required.upload = ['IMAGEUPLOAD']
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,140 +1,140 @@
|
||||
import { app } from "../../scripts/app";
|
||||
import { api } from "../../scripts/api";
|
||||
import { app } from '../../scripts/app'
|
||||
import { api } from '../../scripts/api'
|
||||
|
||||
const WEBCAM_READY = Symbol();
|
||||
const WEBCAM_READY = Symbol()
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.WebcamCapture",
|
||||
name: 'Comfy.WebcamCapture',
|
||||
getCustomWidgets(app) {
|
||||
return {
|
||||
WEBCAM(node, inputName) {
|
||||
let res;
|
||||
node[WEBCAM_READY] = new Promise((resolve) => (res = resolve));
|
||||
let res
|
||||
node[WEBCAM_READY] = new Promise((resolve) => (res = resolve))
|
||||
|
||||
const container = document.createElement("div");
|
||||
container.style.background = "rgba(0,0,0,0.25)";
|
||||
container.style.textAlign = "center";
|
||||
const container = document.createElement('div')
|
||||
container.style.background = 'rgba(0,0,0,0.25)'
|
||||
container.style.textAlign = 'center'
|
||||
|
||||
const video = document.createElement("video");
|
||||
video.style.height = video.style.width = "100%";
|
||||
const video = document.createElement('video')
|
||||
video.style.height = video.style.width = '100%'
|
||||
|
||||
const loadVideo = async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: true,
|
||||
audio: false,
|
||||
});
|
||||
container.replaceChildren(video);
|
||||
audio: false
|
||||
})
|
||||
container.replaceChildren(video)
|
||||
|
||||
setTimeout(() => res(video), 500); // Fallback as loadedmetadata doesnt fire sometimes?
|
||||
video.addEventListener("loadedmetadata", () => res(video), false);
|
||||
video.srcObject = stream;
|
||||
video.play();
|
||||
setTimeout(() => res(video), 500) // Fallback as loadedmetadata doesnt fire sometimes?
|
||||
video.addEventListener('loadedmetadata', () => res(video), false)
|
||||
video.srcObject = stream
|
||||
video.play()
|
||||
} catch (error) {
|
||||
const label = document.createElement("div");
|
||||
label.style.color = "red";
|
||||
label.style.overflow = "auto";
|
||||
label.style.maxHeight = "100%";
|
||||
label.style.whiteSpace = "pre-wrap";
|
||||
const label = document.createElement('div')
|
||||
label.style.color = 'red'
|
||||
label.style.overflow = 'auto'
|
||||
label.style.maxHeight = '100%'
|
||||
label.style.whiteSpace = 'pre-wrap'
|
||||
|
||||
if (window.isSecureContext) {
|
||||
label.textContent =
|
||||
"Unable to load webcam, please ensure access is granted:\n" +
|
||||
error.message;
|
||||
'Unable to load webcam, please ensure access is granted:\n' +
|
||||
error.message
|
||||
} else {
|
||||
label.textContent =
|
||||
"Unable to load webcam. A secure context is required, if you are not accessing ComfyUI on localhost (127.0.0.1) you will have to enable TLS (https)\n\n" +
|
||||
error.message;
|
||||
'Unable to load webcam. A secure context is required, if you are not accessing ComfyUI on localhost (127.0.0.1) you will have to enable TLS (https)\n\n' +
|
||||
error.message
|
||||
}
|
||||
|
||||
container.replaceChildren(label);
|
||||
container.replaceChildren(label)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadVideo();
|
||||
loadVideo()
|
||||
|
||||
return { widget: node.addDOMWidget(inputName, "WEBCAM", container) };
|
||||
},
|
||||
};
|
||||
return { widget: node.addDOMWidget(inputName, 'WEBCAM', container) }
|
||||
}
|
||||
}
|
||||
},
|
||||
nodeCreated(node) {
|
||||
if ((node.type, node.constructor.comfyClass !== "WebcamCapture")) return;
|
||||
if ((node.type, node.constructor.comfyClass !== 'WebcamCapture')) return
|
||||
|
||||
let video;
|
||||
const camera = node.widgets.find((w) => w.name === "image");
|
||||
const w = node.widgets.find((w) => w.name === "width");
|
||||
const h = node.widgets.find((w) => w.name === "height");
|
||||
let video
|
||||
const camera = node.widgets.find((w) => w.name === 'image')
|
||||
const w = node.widgets.find((w) => w.name === 'width')
|
||||
const h = node.widgets.find((w) => w.name === 'height')
|
||||
const captureOnQueue = node.widgets.find(
|
||||
(w) => w.name === "capture_on_queue"
|
||||
);
|
||||
(w) => w.name === 'capture_on_queue'
|
||||
)
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
const canvas = document.createElement('canvas')
|
||||
|
||||
const capture = () => {
|
||||
canvas.width = w.value;
|
||||
canvas.height = h.value;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(video, 0, 0, w.value, h.value);
|
||||
const data = canvas.toDataURL("image/png");
|
||||
canvas.width = w.value
|
||||
canvas.height = h.value
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.drawImage(video, 0, 0, w.value, h.value)
|
||||
const data = canvas.toDataURL('image/png')
|
||||
|
||||
const img = new Image();
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
node.imgs = [img];
|
||||
app.graph.setDirtyCanvas(true);
|
||||
node.imgs = [img]
|
||||
app.graph.setDirtyCanvas(true)
|
||||
requestAnimationFrame(() => {
|
||||
node.setSizeForImage?.();
|
||||
});
|
||||
};
|
||||
img.src = data;
|
||||
};
|
||||
node.setSizeForImage?.()
|
||||
})
|
||||
}
|
||||
img.src = data
|
||||
}
|
||||
|
||||
const btn = node.addWidget(
|
||||
"button",
|
||||
"waiting for camera...",
|
||||
"capture",
|
||||
'button',
|
||||
'waiting for camera...',
|
||||
'capture',
|
||||
capture
|
||||
);
|
||||
btn.disabled = true;
|
||||
btn.serializeValue = () => undefined;
|
||||
)
|
||||
btn.disabled = true
|
||||
btn.serializeValue = () => undefined
|
||||
|
||||
camera.serializeValue = async () => {
|
||||
if (captureOnQueue.value) {
|
||||
capture();
|
||||
capture()
|
||||
} else if (!node.imgs?.length) {
|
||||
const err = `No webcam image captured`;
|
||||
alert(err);
|
||||
throw new Error(err);
|
||||
const err = `No webcam image captured`
|
||||
alert(err)
|
||||
throw new Error(err)
|
||||
}
|
||||
|
||||
// Upload image to temp storage
|
||||
const blob = await new Promise<Blob>((r) => canvas.toBlob(r));
|
||||
const name = `${+new Date()}.png`;
|
||||
const file = new File([blob], name);
|
||||
const body = new FormData();
|
||||
body.append("image", file);
|
||||
body.append("subfolder", "webcam");
|
||||
body.append("type", "temp");
|
||||
const resp = await api.fetchApi("/upload/image", {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
const blob = await new Promise<Blob>((r) => canvas.toBlob(r))
|
||||
const name = `${+new Date()}.png`
|
||||
const file = new File([blob], name)
|
||||
const body = new FormData()
|
||||
body.append('image', file)
|
||||
body.append('subfolder', 'webcam')
|
||||
body.append('type', 'temp')
|
||||
const resp = await api.fetchApi('/upload/image', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
if (resp.status !== 200) {
|
||||
const err = `Error uploading camera image: ${resp.status} - ${resp.statusText}`;
|
||||
alert(err);
|
||||
throw new Error(err);
|
||||
const err = `Error uploading camera image: ${resp.status} - ${resp.statusText}`
|
||||
alert(err)
|
||||
throw new Error(err)
|
||||
}
|
||||
return `webcam/${name} [temp]`
|
||||
}
|
||||
return `webcam/${name} [temp]`;
|
||||
};
|
||||
|
||||
node[WEBCAM_READY].then((v) => {
|
||||
video = v;
|
||||
video = v
|
||||
// If width isnt specified then use video output resolution
|
||||
if (!w.value) {
|
||||
w.value = video.videoWidth || 640;
|
||||
h.value = video.videoHeight || 480;
|
||||
w.value = video.videoWidth || 640
|
||||
h.value = video.videoHeight || 480
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.label = "capture";
|
||||
});
|
||||
},
|
||||
});
|
||||
btn.disabled = false
|
||||
btn.label = 'capture'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
30
src/i18n.ts
30
src/i18n.ts
@@ -1,27 +1,27 @@
|
||||
import { createI18n } from "vue-i18n";
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
const messages = {
|
||||
en: {
|
||||
sideToolBar: {
|
||||
settings: "Settings",
|
||||
themeToggle: "Toggle Theme",
|
||||
queue: "Queue",
|
||||
},
|
||||
settings: 'Settings',
|
||||
themeToggle: 'Toggle Theme',
|
||||
queue: 'Queue'
|
||||
}
|
||||
},
|
||||
zh: {
|
||||
sideToolBar: {
|
||||
settings: "设置",
|
||||
themeToggle: "主题切换",
|
||||
queue: "队列",
|
||||
},
|
||||
},
|
||||
settings: '设置',
|
||||
themeToggle: '主题切换',
|
||||
queue: '队列'
|
||||
}
|
||||
}
|
||||
// TODO: Add more languages
|
||||
};
|
||||
}
|
||||
|
||||
export const i18n = createI18n({
|
||||
// Must set `false`, as Vue I18n Legacy API is for Vue 2
|
||||
legacy: false,
|
||||
locale: navigator.language.split("-")[0] || "en",
|
||||
fallbackLocale: "en",
|
||||
messages,
|
||||
});
|
||||
locale: navigator.language.split('-')[0] || 'en',
|
||||
fallbackLocale: 'en',
|
||||
messages
|
||||
})
|
||||
|
||||
52
src/main.ts
52
src/main.ts
@@ -1,48 +1,48 @@
|
||||
import { createApp } from "vue";
|
||||
import PrimeVue from "primevue/config";
|
||||
import Aura from "@primevue/themes/aura";
|
||||
import { definePreset } from "@primevue/themes";
|
||||
import ConfirmationService from "primevue/confirmationservice";
|
||||
import ToastService from "primevue/toastservice";
|
||||
import Tooltip from "primevue/tooltip";
|
||||
import "primeicons/primeicons.css";
|
||||
import { createApp } from 'vue'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import { definePreset } from '@primevue/themes'
|
||||
import ConfirmationService from 'primevue/confirmationservice'
|
||||
import ToastService from 'primevue/toastservice'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import 'primeicons/primeicons.css'
|
||||
|
||||
import App from "./App.vue";
|
||||
import { app as comfyApp } from "@/scripts/app";
|
||||
import { createPinia } from "pinia";
|
||||
import { i18n } from "./i18n";
|
||||
import App from './App.vue'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { createPinia } from 'pinia'
|
||||
import { i18n } from './i18n'
|
||||
|
||||
const ComfyUIPreset = definePreset(Aura, {
|
||||
semantic: {
|
||||
// @ts-ignore
|
||||
primary: Aura.primitive.blue,
|
||||
},
|
||||
});
|
||||
primary: Aura.primitive.blue
|
||||
}
|
||||
})
|
||||
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
comfyApp.setup().then(() => {
|
||||
window["app"] = comfyApp;
|
||||
window["graph"] = comfyApp.graph;
|
||||
window['app'] = comfyApp
|
||||
window['graph'] = comfyApp.graph
|
||||
|
||||
app.directive("tooltip", Tooltip);
|
||||
app.directive('tooltip', Tooltip)
|
||||
app
|
||||
.use(PrimeVue, {
|
||||
theme: {
|
||||
preset: ComfyUIPreset,
|
||||
options: {
|
||||
prefix: "p",
|
||||
prefix: 'p',
|
||||
cssLayer: false,
|
||||
// This is a workaround for the issue with the dark mode selector
|
||||
// https://github.com/primefaces/primevue/issues/5515
|
||||
darkModeSelector: ".dark-theme, :root:has(.dark-theme)",
|
||||
},
|
||||
},
|
||||
darkModeSelector: '.dark-theme, :root:has(.dark-theme)'
|
||||
}
|
||||
}
|
||||
})
|
||||
.use(ConfirmationService)
|
||||
.use(ToastService)
|
||||
.use(pinia)
|
||||
.use(i18n)
|
||||
.mount("#vue-app");
|
||||
});
|
||||
.mount('#vue-app')
|
||||
})
|
||||
|
||||
@@ -1,64 +1,64 @@
|
||||
import { ComfyWorkflowJSON } from "@/types/comfyWorkflow";
|
||||
import { ComfyWorkflowJSON } from '@/types/comfyWorkflow'
|
||||
import {
|
||||
HistoryTaskItem,
|
||||
PendingTaskItem,
|
||||
RunningTaskItem,
|
||||
ComfyNodeDef,
|
||||
validateComfyNodeDef,
|
||||
} from "@/types/apiTypes";
|
||||
validateComfyNodeDef
|
||||
} from '@/types/apiTypes'
|
||||
|
||||
interface QueuePromptRequestBody {
|
||||
client_id: string;
|
||||
client_id: string
|
||||
// Mapping from node id to node info + input values
|
||||
// TODO: Type this.
|
||||
prompt: Record<number, any>;
|
||||
prompt: Record<number, any>
|
||||
extra_data: {
|
||||
extra_pnginfo: {
|
||||
workflow: ComfyWorkflowJSON;
|
||||
};
|
||||
};
|
||||
front?: boolean;
|
||||
number?: number;
|
||||
workflow: ComfyWorkflowJSON
|
||||
}
|
||||
}
|
||||
front?: boolean
|
||||
number?: number
|
||||
}
|
||||
|
||||
class ComfyApi extends EventTarget {
|
||||
#registered = new Set();
|
||||
api_host: string;
|
||||
api_base: string;
|
||||
initialClientId: string;
|
||||
user: string;
|
||||
socket?: WebSocket;
|
||||
clientId?: string;
|
||||
#registered = new Set()
|
||||
api_host: string
|
||||
api_base: string
|
||||
initialClientId: string
|
||||
user: string
|
||||
socket?: WebSocket
|
||||
clientId?: string
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.api_host = location.host;
|
||||
this.api_base = location.pathname.split("/").slice(0, -1).join("/");
|
||||
this.initialClientId = sessionStorage.getItem("clientId");
|
||||
super()
|
||||
this.api_host = location.host
|
||||
this.api_base = location.pathname.split('/').slice(0, -1).join('/')
|
||||
this.initialClientId = sessionStorage.getItem('clientId')
|
||||
}
|
||||
|
||||
apiURL(route: string): string {
|
||||
return this.api_base + "/api" + route;
|
||||
return this.api_base + '/api' + route
|
||||
}
|
||||
|
||||
fileURL(route: string): string {
|
||||
return this.api_base + route;
|
||||
return this.api_base + route
|
||||
}
|
||||
|
||||
fetchApi(route, options?) {
|
||||
if (!options) {
|
||||
options = {};
|
||||
options = {}
|
||||
}
|
||||
if (!options.headers) {
|
||||
options.headers = {};
|
||||
options.headers = {}
|
||||
}
|
||||
options.headers["Comfy-User"] = this.user;
|
||||
return fetch(this.apiURL(route), options);
|
||||
options.headers['Comfy-User'] = this.user
|
||||
return fetch(this.apiURL(route), options)
|
||||
}
|
||||
|
||||
addEventListener(type, callback, options?) {
|
||||
super.addEventListener(type, callback, options);
|
||||
this.#registered.add(type);
|
||||
super.addEventListener(type, callback, options)
|
||||
this.#registered.add(type)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,13 +67,13 @@ class ComfyApi extends EventTarget {
|
||||
#pollQueue() {
|
||||
setInterval(async () => {
|
||||
try {
|
||||
const resp = await this.fetchApi("/prompt");
|
||||
const status = await resp.json();
|
||||
this.dispatchEvent(new CustomEvent("status", { detail: status }));
|
||||
const resp = await this.fetchApi('/prompt')
|
||||
const status = await resp.json()
|
||||
this.dispatchEvent(new CustomEvent('status', { detail: status }))
|
||||
} catch (error) {
|
||||
this.dispatchEvent(new CustomEvent("status", { detail: null }));
|
||||
this.dispatchEvent(new CustomEvent('status', { detail: null }))
|
||||
}
|
||||
}, 1000);
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,144 +82,144 @@ class ComfyApi extends EventTarget {
|
||||
*/
|
||||
#createSocket(isReconnect?) {
|
||||
if (this.socket) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
let opened = false;
|
||||
let existingSession = window.name;
|
||||
let opened = false
|
||||
let existingSession = window.name
|
||||
if (existingSession) {
|
||||
existingSession = "?clientId=" + existingSession;
|
||||
existingSession = '?clientId=' + existingSession
|
||||
}
|
||||
this.socket = new WebSocket(
|
||||
`ws${window.location.protocol === "https:" ? "s" : ""}://${this.api_host}${this.api_base}/ws${existingSession}`
|
||||
);
|
||||
this.socket.binaryType = "arraybuffer";
|
||||
`ws${window.location.protocol === 'https:' ? 's' : ''}://${this.api_host}${this.api_base}/ws${existingSession}`
|
||||
)
|
||||
this.socket.binaryType = 'arraybuffer'
|
||||
|
||||
this.socket.addEventListener("open", () => {
|
||||
opened = true;
|
||||
this.socket.addEventListener('open', () => {
|
||||
opened = true
|
||||
if (isReconnect) {
|
||||
this.dispatchEvent(new CustomEvent("reconnected"));
|
||||
this.dispatchEvent(new CustomEvent('reconnected'))
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
this.socket.addEventListener("error", () => {
|
||||
if (this.socket) this.socket.close();
|
||||
this.socket.addEventListener('error', () => {
|
||||
if (this.socket) this.socket.close()
|
||||
if (!isReconnect && !opened) {
|
||||
this.#pollQueue();
|
||||
this.#pollQueue()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
this.socket.addEventListener("close", () => {
|
||||
this.socket.addEventListener('close', () => {
|
||||
setTimeout(() => {
|
||||
this.socket = null;
|
||||
this.#createSocket(true);
|
||||
}, 300);
|
||||
this.socket = null
|
||||
this.#createSocket(true)
|
||||
}, 300)
|
||||
if (opened) {
|
||||
this.dispatchEvent(new CustomEvent("status", { detail: null }));
|
||||
this.dispatchEvent(new CustomEvent("reconnecting"));
|
||||
this.dispatchEvent(new CustomEvent('status', { detail: null }))
|
||||
this.dispatchEvent(new CustomEvent('reconnecting'))
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
this.socket.addEventListener("message", (event) => {
|
||||
this.socket.addEventListener('message', (event) => {
|
||||
try {
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
const view = new DataView(event.data);
|
||||
const eventType = view.getUint32(0);
|
||||
const buffer = event.data.slice(4);
|
||||
const view = new DataView(event.data)
|
||||
const eventType = view.getUint32(0)
|
||||
const buffer = event.data.slice(4)
|
||||
switch (eventType) {
|
||||
case 1:
|
||||
const view2 = new DataView(event.data);
|
||||
const imageType = view2.getUint32(0);
|
||||
let imageMime;
|
||||
const view2 = new DataView(event.data)
|
||||
const imageType = view2.getUint32(0)
|
||||
let imageMime
|
||||
switch (imageType) {
|
||||
case 1:
|
||||
default:
|
||||
imageMime = "image/jpeg";
|
||||
break;
|
||||
imageMime = 'image/jpeg'
|
||||
break
|
||||
case 2:
|
||||
imageMime = "image/png";
|
||||
imageMime = 'image/png'
|
||||
}
|
||||
const imageBlob = new Blob([buffer.slice(4)], {
|
||||
type: imageMime,
|
||||
});
|
||||
type: imageMime
|
||||
})
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("b_preview", { detail: imageBlob })
|
||||
);
|
||||
break;
|
||||
new CustomEvent('b_preview', { detail: imageBlob })
|
||||
)
|
||||
break
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown binary websocket message of type ${eventType}`
|
||||
);
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const msg = JSON.parse(event.data);
|
||||
const msg = JSON.parse(event.data)
|
||||
switch (msg.type) {
|
||||
case "status":
|
||||
case 'status':
|
||||
if (msg.data.sid) {
|
||||
this.clientId = msg.data.sid;
|
||||
window.name = this.clientId; // use window name so it isnt reused when duplicating tabs
|
||||
sessionStorage.setItem("clientId", this.clientId); // store in session storage so duplicate tab can load correct workflow
|
||||
this.clientId = msg.data.sid
|
||||
window.name = this.clientId // use window name so it isnt reused when duplicating tabs
|
||||
sessionStorage.setItem('clientId', this.clientId) // store in session storage so duplicate tab can load correct workflow
|
||||
}
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("status", { detail: msg.data.status })
|
||||
);
|
||||
break;
|
||||
case "progress":
|
||||
new CustomEvent('status', { detail: msg.data.status })
|
||||
)
|
||||
break
|
||||
case 'progress':
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("progress", { detail: msg.data })
|
||||
);
|
||||
break;
|
||||
case "executing":
|
||||
new CustomEvent('progress', { detail: msg.data })
|
||||
)
|
||||
break
|
||||
case 'executing':
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("executing", { detail: msg.data.node })
|
||||
);
|
||||
break;
|
||||
case "executed":
|
||||
new CustomEvent('executing', { detail: msg.data.node })
|
||||
)
|
||||
break
|
||||
case 'executed':
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("executed", { detail: msg.data })
|
||||
);
|
||||
break;
|
||||
case "execution_start":
|
||||
new CustomEvent('executed', { detail: msg.data })
|
||||
)
|
||||
break
|
||||
case 'execution_start':
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("execution_start", { detail: msg.data })
|
||||
);
|
||||
break;
|
||||
case "execution_success":
|
||||
new CustomEvent('execution_start', { detail: msg.data })
|
||||
)
|
||||
break
|
||||
case 'execution_success':
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("execution_success", { detail: msg.data })
|
||||
);
|
||||
break;
|
||||
case "execution_error":
|
||||
new CustomEvent('execution_success', { detail: msg.data })
|
||||
)
|
||||
break
|
||||
case 'execution_error':
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("execution_error", { detail: msg.data })
|
||||
);
|
||||
break;
|
||||
case "execution_cached":
|
||||
new CustomEvent('execution_error', { detail: msg.data })
|
||||
)
|
||||
break
|
||||
case 'execution_cached':
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("execution_cached", { detail: msg.data })
|
||||
);
|
||||
break;
|
||||
new CustomEvent('execution_cached', { detail: msg.data })
|
||||
)
|
||||
break
|
||||
default:
|
||||
if (this.#registered.has(msg.type)) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(msg.type, { detail: msg.data })
|
||||
);
|
||||
)
|
||||
} else {
|
||||
throw new Error(`Unknown message type ${msg.type}`);
|
||||
throw new Error(`Unknown message type ${msg.type}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Unhandled message:", event.data, error);
|
||||
console.warn('Unhandled message:', event.data, error)
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises sockets and realtime updates
|
||||
*/
|
||||
init() {
|
||||
this.#createSocket();
|
||||
this.#createSocket()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,8 +227,8 @@ class ComfyApi extends EventTarget {
|
||||
* @returns An array of script urls to import
|
||||
*/
|
||||
async getExtensions() {
|
||||
const resp = await this.fetchApi("/extensions", { cache: "no-store" });
|
||||
return await resp.json();
|
||||
const resp = await this.fetchApi('/extensions', { cache: 'no-store' })
|
||||
return await resp.json()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,8 +236,8 @@ class ComfyApi extends EventTarget {
|
||||
* @returns An array of script urls to import
|
||||
*/
|
||||
async getEmbeddings() {
|
||||
const resp = await this.fetchApi("/embeddings", { cache: "no-store" });
|
||||
return await resp.json();
|
||||
const resp = await this.fetchApi('/embeddings', { cache: 'no-store' })
|
||||
return await resp.json()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,18 +245,18 @@ class ComfyApi extends EventTarget {
|
||||
* @returns The node definitions
|
||||
*/
|
||||
async getNodeDefs(): Promise<Record<string, ComfyNodeDef>> {
|
||||
const resp = await this.fetchApi("/object_info", { cache: "no-store" });
|
||||
const objectInfoUnsafe = await resp.json();
|
||||
const objectInfo: Record<string, ComfyNodeDef> = {};
|
||||
const resp = await this.fetchApi('/object_info', { cache: 'no-store' })
|
||||
const objectInfoUnsafe = await resp.json()
|
||||
const objectInfo: Record<string, ComfyNodeDef> = {}
|
||||
for (const key in objectInfoUnsafe) {
|
||||
try {
|
||||
objectInfo[key] = validateComfyNodeDef(objectInfoUnsafe[key]);
|
||||
objectInfo[key] = validateComfyNodeDef(objectInfoUnsafe[key])
|
||||
} catch (e) {
|
||||
console.warn("Ignore node definition: ", key);
|
||||
console.error(e);
|
||||
console.warn('Ignore node definition: ', key)
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
return objectInfo;
|
||||
return objectInfo
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -268,30 +268,30 @@ class ComfyApi extends EventTarget {
|
||||
const body: QueuePromptRequestBody = {
|
||||
client_id: this.clientId,
|
||||
prompt: output,
|
||||
extra_data: { extra_pnginfo: { workflow } },
|
||||
};
|
||||
|
||||
if (number === -1) {
|
||||
body.front = true;
|
||||
} else if (number != 0) {
|
||||
body.number = number;
|
||||
extra_data: { extra_pnginfo: { workflow } }
|
||||
}
|
||||
|
||||
const res = await this.fetchApi("/prompt", {
|
||||
method: "POST",
|
||||
if (number === -1) {
|
||||
body.front = true
|
||||
} else if (number != 0) {
|
||||
body.number = number
|
||||
}
|
||||
|
||||
const res = await this.fetchApi('/prompt', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw {
|
||||
response: await res.json(),
|
||||
};
|
||||
response: await res.json()
|
||||
}
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
return await res.json()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -300,10 +300,10 @@ class ComfyApi extends EventTarget {
|
||||
* @returns The items of the specified type grouped by their status
|
||||
*/
|
||||
async getItems(type) {
|
||||
if (type === "queue") {
|
||||
return this.getQueue();
|
||||
if (type === 'queue') {
|
||||
return this.getQueue()
|
||||
}
|
||||
return this.getHistory();
|
||||
return this.getHistory()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -311,27 +311,27 @@ class ComfyApi extends EventTarget {
|
||||
* @returns The currently running and queued items
|
||||
*/
|
||||
async getQueue(): Promise<{
|
||||
Running: RunningTaskItem[];
|
||||
Pending: PendingTaskItem[];
|
||||
Running: RunningTaskItem[]
|
||||
Pending: PendingTaskItem[]
|
||||
}> {
|
||||
try {
|
||||
const res = await this.fetchApi("/queue");
|
||||
const data = await res.json();
|
||||
const res = await this.fetchApi('/queue')
|
||||
const data = await res.json()
|
||||
return {
|
||||
// Running action uses a different endpoint for cancelling
|
||||
Running: data.queue_running.map((prompt) => ({
|
||||
taskType: "Running",
|
||||
taskType: 'Running',
|
||||
prompt,
|
||||
remove: { name: "Cancel", cb: () => api.interrupt() },
|
||||
remove: { name: 'Cancel', cb: () => api.interrupt() }
|
||||
})),
|
||||
Pending: data.queue_pending.map((prompt) => ({
|
||||
taskType: "Pending",
|
||||
prompt,
|
||||
})),
|
||||
};
|
||||
taskType: 'Pending',
|
||||
prompt
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { Running: [], Pending: [] };
|
||||
console.error(error)
|
||||
return { Running: [], Pending: [] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,15 +343,15 @@ class ComfyApi extends EventTarget {
|
||||
max_items: number = 200
|
||||
): Promise<{ History: HistoryTaskItem[] }> {
|
||||
try {
|
||||
const res = await this.fetchApi(`/history?max_items=${max_items}`);
|
||||
const res = await this.fetchApi(`/history?max_items=${max_items}`)
|
||||
return {
|
||||
History: Object.values(await res.json()).map(
|
||||
(item: HistoryTaskItem) => ({ ...item, taskType: "History" })
|
||||
),
|
||||
};
|
||||
(item: HistoryTaskItem) => ({ ...item, taskType: 'History' })
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { History: [] };
|
||||
console.error(error)
|
||||
return { History: [] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,8 +360,8 @@ class ComfyApi extends EventTarget {
|
||||
* @returns System stats such as python version, OS, per device info
|
||||
*/
|
||||
async getSystemStats() {
|
||||
const res = await this.fetchApi("/system_stats");
|
||||
return await res.json();
|
||||
const res = await this.fetchApi('/system_stats')
|
||||
return await res.json()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -371,15 +371,15 @@ class ComfyApi extends EventTarget {
|
||||
*/
|
||||
async #postItem(type, body) {
|
||||
try {
|
||||
await this.fetchApi("/" + type, {
|
||||
method: "POST",
|
||||
await this.fetchApi('/' + type, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
body: body ? JSON.stringify(body) : undefined
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,7 +389,7 @@ class ComfyApi extends EventTarget {
|
||||
* @param {number} id The id of the item to delete
|
||||
*/
|
||||
async deleteItem(type, id) {
|
||||
await this.#postItem(type, { delete: [id] });
|
||||
await this.#postItem(type, { delete: [id] })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -397,14 +397,14 @@ class ComfyApi extends EventTarget {
|
||||
* @param {string} type The type of list to clear, queue or history
|
||||
*/
|
||||
async clearItems(type) {
|
||||
await this.#postItem(type, { clear: true });
|
||||
await this.#postItem(type, { clear: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Interrupts the execution of the running prompt
|
||||
*/
|
||||
async interrupt() {
|
||||
await this.#postItem("interrupt", null);
|
||||
await this.#postItem('interrupt', null)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -412,7 +412,7 @@ class ComfyApi extends EventTarget {
|
||||
* @returns { Promise<{ storage: "server" | "browser", users?: Promise<string, unknown>, migrated?: boolean }> }
|
||||
*/
|
||||
async getUserConfig() {
|
||||
return (await this.fetchApi("/users")).json();
|
||||
return (await this.fetchApi('/users')).json()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -421,13 +421,13 @@ class ComfyApi extends EventTarget {
|
||||
* @returns The fetch response
|
||||
*/
|
||||
createUser(username) {
|
||||
return this.fetchApi("/users", {
|
||||
method: "POST",
|
||||
return this.fetchApi('/users', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ username }),
|
||||
});
|
||||
body: JSON.stringify({ username })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -435,7 +435,7 @@ class ComfyApi extends EventTarget {
|
||||
* @returns { Promise<string, unknown> } A dictionary of id -> value
|
||||
*/
|
||||
async getSettings() {
|
||||
return (await this.fetchApi("/settings")).json();
|
||||
return (await this.fetchApi('/settings')).json()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -444,7 +444,7 @@ class ComfyApi extends EventTarget {
|
||||
* @returns { Promise<unknown> } The setting value
|
||||
*/
|
||||
async getSetting(id) {
|
||||
return (await this.fetchApi(`/settings/${encodeURIComponent(id)}`)).json();
|
||||
return (await this.fetchApi(`/settings/${encodeURIComponent(id)}`)).json()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -454,9 +454,9 @@ class ComfyApi extends EventTarget {
|
||||
*/
|
||||
async storeSettings(settings) {
|
||||
return this.fetchApi(`/settings`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
method: 'POST',
|
||||
body: JSON.stringify(settings)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -467,9 +467,9 @@ class ComfyApi extends EventTarget {
|
||||
*/
|
||||
async storeSetting(id, value) {
|
||||
return this.fetchApi(`/settings/${encodeURIComponent(id)}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(value),
|
||||
});
|
||||
method: 'POST',
|
||||
body: JSON.stringify(value)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -479,7 +479,7 @@ class ComfyApi extends EventTarget {
|
||||
* @returns { Promise<unknown> } The fetch response object
|
||||
*/
|
||||
async getUserData(file, options?) {
|
||||
return this.fetchApi(`/userdata/${encodeURIComponent(file)}`, options);
|
||||
return this.fetchApi(`/userdata/${encodeURIComponent(file)}`, options)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -493,26 +493,26 @@ class ComfyApi extends EventTarget {
|
||||
file: string,
|
||||
data: unknown,
|
||||
options: RequestInit & {
|
||||
overwrite?: boolean;
|
||||
stringify?: boolean;
|
||||
throwOnError?: boolean;
|
||||
overwrite?: boolean
|
||||
stringify?: boolean
|
||||
throwOnError?: boolean
|
||||
} = { overwrite: true, stringify: true, throwOnError: true }
|
||||
): Promise<Response> {
|
||||
const resp = await this.fetchApi(
|
||||
`/userdata/${encodeURIComponent(file)}?overwrite=${options.overwrite}`,
|
||||
{
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
body: options?.stringify ? JSON.stringify(data) : data,
|
||||
...options,
|
||||
...options
|
||||
}
|
||||
);
|
||||
)
|
||||
if (resp.status !== 200 && options.throwOnError !== false) {
|
||||
throw new Error(
|
||||
`Error storing user data file '${file}': ${resp.status} ${(await resp).statusText}`
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return resp;
|
||||
return resp
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -521,12 +521,12 @@ class ComfyApi extends EventTarget {
|
||||
*/
|
||||
async deleteUserData(file) {
|
||||
const resp = await this.fetchApi(`/userdata/${encodeURIComponent(file)}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
method: 'DELETE'
|
||||
})
|
||||
if (resp.status !== 204) {
|
||||
throw new Error(
|
||||
`Error removing user data file '${file}': ${resp.status} ${resp.statusText}`
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,10 +539,10 @@ class ComfyApi extends EventTarget {
|
||||
const resp = await this.fetchApi(
|
||||
`/userdata/${encodeURIComponent(source)}/move/${encodeURIComponent(dest)}?overwrite=${options?.overwrite}`,
|
||||
{
|
||||
method: "POST",
|
||||
method: 'POST'
|
||||
}
|
||||
);
|
||||
return resp;
|
||||
)
|
||||
return resp
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -566,17 +566,17 @@ class ComfyApi extends EventTarget {
|
||||
`/userdata?${new URLSearchParams({
|
||||
recurse,
|
||||
dir,
|
||||
split,
|
||||
split
|
||||
})}`
|
||||
);
|
||||
if (resp.status === 404) return [];
|
||||
)
|
||||
if (resp.status === 404) return []
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(
|
||||
`Error getting user data list '${dir}': ${resp.status} ${resp.statusText}`
|
||||
);
|
||||
)
|
||||
}
|
||||
return resp.json();
|
||||
return resp.json()
|
||||
}
|
||||
}
|
||||
|
||||
export const api = new ComfyApi();
|
||||
export const api = new ComfyApi()
|
||||
|
||||
2376
src/scripts/app.ts
2376
src/scripts/app.ts
File diff suppressed because it is too large
Load Diff
@@ -1,278 +1,278 @@
|
||||
import type { ComfyApp } from "./app";
|
||||
import { api } from "./api";
|
||||
import { clone } from "./utils";
|
||||
import { LGraphCanvas, LiteGraph } from "@comfyorg/litegraph";
|
||||
import { ComfyWorkflow } from "./workflows";
|
||||
import type { ComfyApp } from './app'
|
||||
import { api } from './api'
|
||||
import { clone } from './utils'
|
||||
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
||||
import { ComfyWorkflow } from './workflows'
|
||||
|
||||
export class ChangeTracker {
|
||||
static MAX_HISTORY = 50;
|
||||
#app: ComfyApp;
|
||||
undo = [];
|
||||
redo = [];
|
||||
activeState = null;
|
||||
isOurLoad = false;
|
||||
workflow: ComfyWorkflow | null;
|
||||
static MAX_HISTORY = 50
|
||||
#app: ComfyApp
|
||||
undo = []
|
||||
redo = []
|
||||
activeState = null
|
||||
isOurLoad = false
|
||||
workflow: ComfyWorkflow | null
|
||||
|
||||
ds: { scale: number; offset: [number, number] };
|
||||
nodeOutputs: any;
|
||||
ds: { scale: number; offset: [number, number] }
|
||||
nodeOutputs: any
|
||||
|
||||
get app() {
|
||||
return this.#app ?? this.workflow.manager.app;
|
||||
return this.#app ?? this.workflow.manager.app
|
||||
}
|
||||
|
||||
constructor(workflow: ComfyWorkflow) {
|
||||
this.workflow = workflow;
|
||||
this.workflow = workflow
|
||||
}
|
||||
|
||||
#setApp(app) {
|
||||
this.#app = app;
|
||||
this.#app = app
|
||||
}
|
||||
|
||||
store() {
|
||||
this.ds = {
|
||||
scale: this.app.canvas.ds.scale,
|
||||
offset: [...this.app.canvas.ds.offset],
|
||||
};
|
||||
offset: [...this.app.canvas.ds.offset]
|
||||
}
|
||||
}
|
||||
|
||||
restore() {
|
||||
if (this.ds) {
|
||||
this.app.canvas.ds.scale = this.ds.scale;
|
||||
this.app.canvas.ds.offset = this.ds.offset;
|
||||
this.app.canvas.ds.scale = this.ds.scale
|
||||
this.app.canvas.ds.offset = this.ds.offset
|
||||
}
|
||||
if (this.nodeOutputs) {
|
||||
this.app.nodeOutputs = this.nodeOutputs;
|
||||
this.app.nodeOutputs = this.nodeOutputs
|
||||
}
|
||||
}
|
||||
|
||||
checkState() {
|
||||
if (!this.app.graph) return;
|
||||
if (!this.app.graph) return
|
||||
|
||||
const currentState = this.app.graph.serialize();
|
||||
const currentState = this.app.graph.serialize()
|
||||
if (!this.activeState) {
|
||||
this.activeState = clone(currentState);
|
||||
return;
|
||||
this.activeState = clone(currentState)
|
||||
return
|
||||
}
|
||||
if (!ChangeTracker.graphEqual(this.activeState, currentState)) {
|
||||
this.undo.push(this.activeState);
|
||||
this.undo.push(this.activeState)
|
||||
if (this.undo.length > ChangeTracker.MAX_HISTORY) {
|
||||
this.undo.shift();
|
||||
this.undo.shift()
|
||||
}
|
||||
this.activeState = clone(currentState);
|
||||
this.redo.length = 0;
|
||||
this.workflow.unsaved = true;
|
||||
this.activeState = clone(currentState)
|
||||
this.redo.length = 0
|
||||
this.workflow.unsaved = true
|
||||
api.dispatchEvent(
|
||||
new CustomEvent("graphChanged", { detail: this.activeState })
|
||||
);
|
||||
new CustomEvent('graphChanged', { detail: this.activeState })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async updateState(source, target) {
|
||||
const prevState = source.pop();
|
||||
const prevState = source.pop()
|
||||
if (prevState) {
|
||||
target.push(this.activeState);
|
||||
this.isOurLoad = true;
|
||||
await this.app.loadGraphData(prevState, false, false, this.workflow);
|
||||
this.activeState = prevState;
|
||||
target.push(this.activeState)
|
||||
this.isOurLoad = true
|
||||
await this.app.loadGraphData(prevState, false, false, this.workflow)
|
||||
this.activeState = prevState
|
||||
}
|
||||
}
|
||||
|
||||
async undoRedo(e) {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
if (e.key === "y") {
|
||||
this.updateState(this.redo, this.undo);
|
||||
return true;
|
||||
} else if (e.key === "z") {
|
||||
this.updateState(this.undo, this.redo);
|
||||
return true;
|
||||
if (e.key === 'y') {
|
||||
this.updateState(this.redo, this.undo)
|
||||
return true
|
||||
} else if (e.key === 'z') {
|
||||
this.updateState(this.undo, this.redo)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static init(app: ComfyApp) {
|
||||
const changeTracker = () =>
|
||||
app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker;
|
||||
globalTracker.#setApp(app);
|
||||
app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker
|
||||
globalTracker.#setApp(app)
|
||||
|
||||
const loadGraphData = app.loadGraphData;
|
||||
const loadGraphData = app.loadGraphData
|
||||
app.loadGraphData = async function () {
|
||||
const v = await loadGraphData.apply(this, arguments);
|
||||
const ct = changeTracker();
|
||||
const v = await loadGraphData.apply(this, arguments)
|
||||
const ct = changeTracker()
|
||||
if (ct.isOurLoad) {
|
||||
ct.isOurLoad = false;
|
||||
ct.isOurLoad = false
|
||||
} else {
|
||||
ct.checkState();
|
||||
ct.checkState()
|
||||
}
|
||||
return v
|
||||
}
|
||||
return v;
|
||||
};
|
||||
|
||||
let keyIgnored = false;
|
||||
let keyIgnored = false
|
||||
window.addEventListener(
|
||||
"keydown",
|
||||
'keydown',
|
||||
(e) => {
|
||||
requestAnimationFrame(async () => {
|
||||
let activeEl;
|
||||
let activeEl
|
||||
// If we are auto queue in change mode then we do want to trigger on inputs
|
||||
if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === "instant") {
|
||||
activeEl = document.activeElement;
|
||||
if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === 'instant') {
|
||||
activeEl = document.activeElement
|
||||
if (
|
||||
activeEl?.tagName === "INPUT" ||
|
||||
activeEl?.["type"] === "textarea"
|
||||
activeEl?.tagName === 'INPUT' ||
|
||||
activeEl?.['type'] === 'textarea'
|
||||
) {
|
||||
// Ignore events on inputs, they have their native history
|
||||
return;
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
keyIgnored =
|
||||
e.key === "Control" ||
|
||||
e.key === "Shift" ||
|
||||
e.key === "Alt" ||
|
||||
e.key === "Meta";
|
||||
if (keyIgnored) return;
|
||||
e.key === 'Control' ||
|
||||
e.key === 'Shift' ||
|
||||
e.key === 'Alt' ||
|
||||
e.key === 'Meta'
|
||||
if (keyIgnored) return
|
||||
|
||||
// Check if this is a ctrl+z ctrl+y
|
||||
if (await changeTracker().undoRedo(e)) return;
|
||||
if (await changeTracker().undoRedo(e)) return
|
||||
|
||||
// If our active element is some type of input then handle changes after they're done
|
||||
if (ChangeTracker.bindInput(app, activeEl)) return;
|
||||
changeTracker().checkState();
|
||||
});
|
||||
if (ChangeTracker.bindInput(app, activeEl)) return
|
||||
changeTracker().checkState()
|
||||
})
|
||||
},
|
||||
true
|
||||
);
|
||||
)
|
||||
|
||||
window.addEventListener("keyup", (e) => {
|
||||
window.addEventListener('keyup', (e) => {
|
||||
if (keyIgnored) {
|
||||
keyIgnored = false;
|
||||
changeTracker().checkState();
|
||||
keyIgnored = false
|
||||
changeTracker().checkState()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// Handle clicking DOM elements (e.g. widgets)
|
||||
window.addEventListener("mouseup", () => {
|
||||
changeTracker().checkState();
|
||||
});
|
||||
window.addEventListener('mouseup', () => {
|
||||
changeTracker().checkState()
|
||||
})
|
||||
|
||||
// Handle prompt queue event for dynamic widget changes
|
||||
api.addEventListener("promptQueued", () => {
|
||||
changeTracker().checkState();
|
||||
});
|
||||
api.addEventListener('promptQueued', () => {
|
||||
changeTracker().checkState()
|
||||
})
|
||||
|
||||
api.addEventListener("graphCleared", () => {
|
||||
changeTracker().checkState();
|
||||
});
|
||||
api.addEventListener('graphCleared', () => {
|
||||
changeTracker().checkState()
|
||||
})
|
||||
|
||||
// Handle litegraph clicks
|
||||
// @ts-ignore
|
||||
const processMouseUp = LGraphCanvas.prototype.processMouseUp;
|
||||
const processMouseUp = LGraphCanvas.prototype.processMouseUp
|
||||
// @ts-ignore
|
||||
LGraphCanvas.prototype.processMouseUp = function (e) {
|
||||
const v = processMouseUp.apply(this, arguments);
|
||||
changeTracker().checkState();
|
||||
return v;
|
||||
};
|
||||
const v = processMouseUp.apply(this, arguments)
|
||||
changeTracker().checkState()
|
||||
return v
|
||||
}
|
||||
// @ts-ignore
|
||||
const processMouseDown = LGraphCanvas.prototype.processMouseDown;
|
||||
const processMouseDown = LGraphCanvas.prototype.processMouseDown
|
||||
// @ts-ignore
|
||||
LGraphCanvas.prototype.processMouseDown = function (e) {
|
||||
const v = processMouseDown.apply(this, arguments);
|
||||
changeTracker().checkState();
|
||||
return v;
|
||||
};
|
||||
const v = processMouseDown.apply(this, arguments)
|
||||
changeTracker().checkState()
|
||||
return v
|
||||
}
|
||||
|
||||
// Handle litegraph context menu for COMBO widgets
|
||||
const close = LiteGraph.ContextMenu.prototype.close;
|
||||
const close = LiteGraph.ContextMenu.prototype.close
|
||||
LiteGraph.ContextMenu.prototype.close = function (e) {
|
||||
const v = close.apply(this, arguments);
|
||||
changeTracker().checkState();
|
||||
return v;
|
||||
};
|
||||
const v = close.apply(this, arguments)
|
||||
changeTracker().checkState()
|
||||
return v
|
||||
}
|
||||
|
||||
// Detects nodes being added via the node search dialog
|
||||
const onNodeAdded = LiteGraph.LGraph.prototype.onNodeAdded;
|
||||
const onNodeAdded = LiteGraph.LGraph.prototype.onNodeAdded
|
||||
LiteGraph.LGraph.prototype.onNodeAdded = function () {
|
||||
const v = onNodeAdded?.apply(this, arguments);
|
||||
const v = onNodeAdded?.apply(this, arguments)
|
||||
if (!app?.configuringGraph) {
|
||||
const ct = changeTracker();
|
||||
const ct = changeTracker()
|
||||
if (!ct.isOurLoad) {
|
||||
ct.checkState();
|
||||
ct.checkState()
|
||||
}
|
||||
}
|
||||
return v;
|
||||
};
|
||||
return v
|
||||
}
|
||||
|
||||
// Store node outputs
|
||||
api.addEventListener("executed", ({ detail }) => {
|
||||
const prompt = app.workflowManager.queuedPrompts[detail.prompt_id];
|
||||
if (!prompt?.workflow) return;
|
||||
const nodeOutputs = (prompt.workflow.changeTracker.nodeOutputs ??= {});
|
||||
const output = nodeOutputs[detail.node];
|
||||
api.addEventListener('executed', ({ detail }) => {
|
||||
const prompt = app.workflowManager.queuedPrompts[detail.prompt_id]
|
||||
if (!prompt?.workflow) return
|
||||
const nodeOutputs = (prompt.workflow.changeTracker.nodeOutputs ??= {})
|
||||
const output = nodeOutputs[detail.node]
|
||||
if (detail.merge && output) {
|
||||
for (const k in detail.output ?? {}) {
|
||||
const v = output[k];
|
||||
const v = output[k]
|
||||
if (v instanceof Array) {
|
||||
output[k] = v.concat(detail.output[k]);
|
||||
output[k] = v.concat(detail.output[k])
|
||||
} else {
|
||||
output[k] = detail.output[k];
|
||||
output[k] = detail.output[k]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
nodeOutputs[detail.node] = detail.output;
|
||||
nodeOutputs[detail.node] = detail.output
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
static bindInput(app, activeEl) {
|
||||
if (
|
||||
activeEl &&
|
||||
activeEl.tagName !== "CANVAS" &&
|
||||
activeEl.tagName !== "BODY"
|
||||
activeEl.tagName !== 'CANVAS' &&
|
||||
activeEl.tagName !== 'BODY'
|
||||
) {
|
||||
for (const evt of ["change", "input", "blur"]) {
|
||||
for (const evt of ['change', 'input', 'blur']) {
|
||||
if (`on${evt}` in activeEl) {
|
||||
const listener = () => {
|
||||
app.workflowManager.activeWorkflow.changeTracker.checkState();
|
||||
activeEl.removeEventListener(evt, listener);
|
||||
};
|
||||
activeEl.addEventListener(evt, listener);
|
||||
return true;
|
||||
app.workflowManager.activeWorkflow.changeTracker.checkState()
|
||||
activeEl.removeEventListener(evt, listener)
|
||||
}
|
||||
activeEl.addEventListener(evt, listener)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static graphEqual(a, b, path = "") {
|
||||
if (a === b) return true;
|
||||
static graphEqual(a, b, path = '') {
|
||||
if (a === b) return true
|
||||
|
||||
if (typeof a == "object" && a && typeof b == "object" && b) {
|
||||
const keys = Object.getOwnPropertyNames(a);
|
||||
if (typeof a == 'object' && a && typeof b == 'object' && b) {
|
||||
const keys = Object.getOwnPropertyNames(a)
|
||||
|
||||
if (keys.length != Object.getOwnPropertyNames(b).length) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
let av = a[key];
|
||||
let bv = b[key];
|
||||
if (!path && key === "nodes") {
|
||||
let av = a[key]
|
||||
let bv = b[key]
|
||||
if (!path && key === 'nodes') {
|
||||
// Nodes need to be sorted as the order changes when selecting nodes
|
||||
av = [...av].sort((a, b) => a.id - b.id);
|
||||
bv = [...bv].sort((a, b) => a.id - b.id);
|
||||
} else if (path === "extra.ds") {
|
||||
av = [...av].sort((a, b) => a.id - b.id)
|
||||
bv = [...bv].sort((a, b) => a.id - b.id)
|
||||
} else if (path === 'extra.ds') {
|
||||
// Ignore view changes
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
if (!ChangeTracker.graphEqual(av, bv, path + (path ? "." : "") + key)) {
|
||||
return false;
|
||||
if (!ChangeTracker.graphEqual(av, bv, path + (path ? '.' : '') + key)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const globalTracker = new ChangeTracker({} as ComfyWorkflow);
|
||||
const globalTracker = new ChangeTracker({} as ComfyWorkflow)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ComfyWorkflowJSON } from "@/types/comfyWorkflow";
|
||||
import type { ComfyWorkflowJSON } from '@/types/comfyWorkflow'
|
||||
|
||||
export const defaultGraph: ComfyWorkflowJSON = {
|
||||
last_node_id: 9,
|
||||
@@ -6,132 +6,132 @@ export const defaultGraph: ComfyWorkflowJSON = {
|
||||
nodes: [
|
||||
{
|
||||
id: 7,
|
||||
type: "CLIPTextEncode",
|
||||
type: 'CLIPTextEncode',
|
||||
pos: [413, 389],
|
||||
size: [425.27801513671875, 180.6060791015625],
|
||||
flags: {},
|
||||
order: 3,
|
||||
mode: 0,
|
||||
inputs: [{ name: "clip", type: "CLIP", link: 5 }],
|
||||
inputs: [{ name: 'clip', type: 'CLIP', link: 5 }],
|
||||
outputs: [
|
||||
{
|
||||
name: "CONDITIONING",
|
||||
type: "CONDITIONING",
|
||||
name: 'CONDITIONING',
|
||||
type: 'CONDITIONING',
|
||||
links: [6],
|
||||
slot_index: 0,
|
||||
},
|
||||
slot_index: 0
|
||||
}
|
||||
],
|
||||
properties: {},
|
||||
widgets_values: ["text, watermark"],
|
||||
widgets_values: ['text, watermark']
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
type: "CLIPTextEncode",
|
||||
type: 'CLIPTextEncode',
|
||||
pos: [415, 186],
|
||||
size: [422.84503173828125, 164.31304931640625],
|
||||
flags: {},
|
||||
order: 2,
|
||||
mode: 0,
|
||||
inputs: [{ name: "clip", type: "CLIP", link: 3 }],
|
||||
inputs: [{ name: 'clip', type: 'CLIP', link: 3 }],
|
||||
outputs: [
|
||||
{
|
||||
name: "CONDITIONING",
|
||||
type: "CONDITIONING",
|
||||
name: 'CONDITIONING',
|
||||
type: 'CONDITIONING',
|
||||
links: [4],
|
||||
slot_index: 0,
|
||||
},
|
||||
slot_index: 0
|
||||
}
|
||||
],
|
||||
properties: {},
|
||||
widgets_values: [
|
||||
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,",
|
||||
],
|
||||
'beautiful scenery nature glass bottle landscape, , purple galaxy bottle,'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
type: "EmptyLatentImage",
|
||||
type: 'EmptyLatentImage',
|
||||
pos: [473, 609],
|
||||
size: [315, 106],
|
||||
flags: {},
|
||||
order: 1,
|
||||
mode: 0,
|
||||
outputs: [{ name: "LATENT", type: "LATENT", links: [2], slot_index: 0 }],
|
||||
outputs: [{ name: 'LATENT', type: 'LATENT', links: [2], slot_index: 0 }],
|
||||
properties: {},
|
||||
widgets_values: [512, 512, 1],
|
||||
widgets_values: [512, 512, 1]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: "KSampler",
|
||||
type: 'KSampler',
|
||||
pos: [863, 186],
|
||||
size: [315, 262],
|
||||
flags: {},
|
||||
order: 4,
|
||||
mode: 0,
|
||||
inputs: [
|
||||
{ name: "model", type: "MODEL", link: 1 },
|
||||
{ name: "positive", type: "CONDITIONING", link: 4 },
|
||||
{ name: "negative", type: "CONDITIONING", link: 6 },
|
||||
{ name: "latent_image", type: "LATENT", link: 2 },
|
||||
{ name: 'model', type: 'MODEL', link: 1 },
|
||||
{ name: 'positive', type: 'CONDITIONING', link: 4 },
|
||||
{ name: 'negative', type: 'CONDITIONING', link: 6 },
|
||||
{ name: 'latent_image', type: 'LATENT', link: 2 }
|
||||
],
|
||||
outputs: [{ name: "LATENT", type: "LATENT", links: [7], slot_index: 0 }],
|
||||
outputs: [{ name: 'LATENT', type: 'LATENT', links: [7], slot_index: 0 }],
|
||||
properties: {},
|
||||
widgets_values: [156680208700286, true, 20, 8, "euler", "normal", 1],
|
||||
widgets_values: [156680208700286, true, 20, 8, 'euler', 'normal', 1]
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
type: "VAEDecode",
|
||||
type: 'VAEDecode',
|
||||
pos: [1209, 188],
|
||||
size: [210, 46],
|
||||
flags: {},
|
||||
order: 5,
|
||||
mode: 0,
|
||||
inputs: [
|
||||
{ name: "samples", type: "LATENT", link: 7 },
|
||||
{ name: "vae", type: "VAE", link: 8 },
|
||||
{ name: 'samples', type: 'LATENT', link: 7 },
|
||||
{ name: 'vae', type: 'VAE', link: 8 }
|
||||
],
|
||||
outputs: [{ name: "IMAGE", type: "IMAGE", links: [9], slot_index: 0 }],
|
||||
properties: {},
|
||||
outputs: [{ name: 'IMAGE', type: 'IMAGE', links: [9], slot_index: 0 }],
|
||||
properties: {}
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
type: "SaveImage",
|
||||
type: 'SaveImage',
|
||||
pos: [1451, 189],
|
||||
size: [210, 26],
|
||||
flags: {},
|
||||
order: 6,
|
||||
mode: 0,
|
||||
inputs: [{ name: "images", type: "IMAGE", link: 9 }],
|
||||
properties: {},
|
||||
inputs: [{ name: 'images', type: 'IMAGE', link: 9 }],
|
||||
properties: {}
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: "CheckpointLoaderSimple",
|
||||
type: 'CheckpointLoaderSimple',
|
||||
pos: [26, 474],
|
||||
size: [315, 98],
|
||||
flags: {},
|
||||
order: 0,
|
||||
mode: 0,
|
||||
outputs: [
|
||||
{ name: "MODEL", type: "MODEL", links: [1], slot_index: 0 },
|
||||
{ name: "CLIP", type: "CLIP", links: [3, 5], slot_index: 1 },
|
||||
{ name: "VAE", type: "VAE", links: [8], slot_index: 2 },
|
||||
{ name: 'MODEL', type: 'MODEL', links: [1], slot_index: 0 },
|
||||
{ name: 'CLIP', type: 'CLIP', links: [3, 5], slot_index: 1 },
|
||||
{ name: 'VAE', type: 'VAE', links: [8], slot_index: 2 }
|
||||
],
|
||||
properties: {},
|
||||
widgets_values: ["v1-5-pruned-emaonly.ckpt"],
|
||||
},
|
||||
widgets_values: ['v1-5-pruned-emaonly.ckpt']
|
||||
}
|
||||
],
|
||||
links: [
|
||||
[1, 4, 0, 3, 0, "MODEL"],
|
||||
[2, 5, 0, 3, 3, "LATENT"],
|
||||
[3, 4, 1, 6, 0, "CLIP"],
|
||||
[4, 6, 0, 3, 1, "CONDITIONING"],
|
||||
[5, 4, 1, 7, 0, "CLIP"],
|
||||
[6, 7, 0, 3, 2, "CONDITIONING"],
|
||||
[7, 3, 0, 8, 0, "LATENT"],
|
||||
[8, 4, 2, 8, 1, "VAE"],
|
||||
[9, 8, 0, 9, 0, "IMAGE"],
|
||||
[1, 4, 0, 3, 0, 'MODEL'],
|
||||
[2, 5, 0, 3, 3, 'LATENT'],
|
||||
[3, 4, 1, 6, 0, 'CLIP'],
|
||||
[4, 6, 0, 3, 1, 'CONDITIONING'],
|
||||
[5, 4, 1, 7, 0, 'CLIP'],
|
||||
[6, 7, 0, 3, 2, 'CONDITIONING'],
|
||||
[7, 3, 0, 8, 0, 'LATENT'],
|
||||
[8, 4, 2, 8, 1, 'VAE'],
|
||||
[9, 8, 0, 9, 0, 'IMAGE']
|
||||
],
|
||||
groups: [],
|
||||
config: {},
|
||||
extra: {},
|
||||
version: 0.4,
|
||||
};
|
||||
version: 0.4
|
||||
}
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
import { app, ANIM_PREVIEW_WIDGET } from "./app";
|
||||
import { LGraphCanvas, LGraphNode, LiteGraph } from "@comfyorg/litegraph";
|
||||
import type { Vector4 } from "@comfyorg/litegraph";
|
||||
import { app, ANIM_PREVIEW_WIDGET } from './app'
|
||||
import { LGraphCanvas, LGraphNode, LiteGraph } from '@comfyorg/litegraph'
|
||||
import type { Vector4 } from '@comfyorg/litegraph'
|
||||
|
||||
const SIZE = Symbol();
|
||||
const SIZE = Symbol()
|
||||
|
||||
interface Rect {
|
||||
height: number;
|
||||
width: number;
|
||||
x: number;
|
||||
y: number;
|
||||
height: number
|
||||
width: number
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface DOMWidget<T = HTMLElement> {
|
||||
type: string;
|
||||
name: string;
|
||||
computedHeight?: number;
|
||||
element?: T;
|
||||
options: any;
|
||||
value?: any;
|
||||
y?: number;
|
||||
callback?: (value: any) => void;
|
||||
type: string
|
||||
name: string
|
||||
computedHeight?: number
|
||||
element?: T
|
||||
options: any
|
||||
value?: any
|
||||
y?: number
|
||||
callback?: (value: any) => void
|
||||
draw?: (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
node: LGraphNode,
|
||||
widgetWidth: number,
|
||||
y: number,
|
||||
widgetHeight: number
|
||||
) => void;
|
||||
onRemove?: () => void;
|
||||
) => void
|
||||
onRemove?: () => void
|
||||
}
|
||||
|
||||
function intersect(a: Rect, b: Rect): Vector4 | null {
|
||||
const x = Math.max(a.x, b.x);
|
||||
const num1 = Math.min(a.x + a.width, b.x + b.width);
|
||||
const y = Math.max(a.y, b.y);
|
||||
const num2 = Math.min(a.y + a.height, b.y + b.height);
|
||||
if (num1 >= x && num2 >= y) return [x, y, num1 - x, num2 - y];
|
||||
else return null;
|
||||
const x = Math.max(a.x, b.x)
|
||||
const num1 = Math.min(a.x + a.width, b.x + b.width)
|
||||
const y = Math.max(a.y, b.y)
|
||||
const num2 = Math.min(a.y + a.height, b.y + b.height)
|
||||
if (num1 >= x && num2 >= y) return [x, y, num1 - x, num2 - y]
|
||||
else return null
|
||||
}
|
||||
|
||||
function getClipPath(node: LGraphNode, element: HTMLElement): string {
|
||||
const selectedNode: LGraphNode = Object.values(
|
||||
app.canvas.selected_nodes
|
||||
)[0] as LGraphNode;
|
||||
)[0] as LGraphNode
|
||||
if (selectedNode && selectedNode !== node) {
|
||||
const elRect = element.getBoundingClientRect();
|
||||
const MARGIN = 7;
|
||||
const scale = app.canvas.ds.scale;
|
||||
const elRect = element.getBoundingClientRect()
|
||||
const MARGIN = 7
|
||||
const scale = app.canvas.ds.scale
|
||||
|
||||
const bounding = selectedNode.getBounding();
|
||||
const bounding = selectedNode.getBounding()
|
||||
const intersection = intersect(
|
||||
{
|
||||
x: elRect.x / scale,
|
||||
y: elRect.y / scale,
|
||||
width: elRect.width / scale,
|
||||
height: elRect.height / scale,
|
||||
height: elRect.height / scale
|
||||
},
|
||||
{
|
||||
x: selectedNode.pos[0] + app.canvas.ds.offset[0] - MARGIN,
|
||||
@@ -64,197 +64,197 @@ function getClipPath(node: LGraphNode, element: HTMLElement): string {
|
||||
LiteGraph.NODE_TITLE_HEIGHT -
|
||||
MARGIN,
|
||||
width: bounding[2] + MARGIN + MARGIN,
|
||||
height: bounding[3] + MARGIN + MARGIN,
|
||||
height: bounding[3] + MARGIN + MARGIN
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
if (!intersection) {
|
||||
return "";
|
||||
return ''
|
||||
}
|
||||
|
||||
const widgetRect = element.getBoundingClientRect();
|
||||
const clipX = elRect.left + intersection[0] - widgetRect.x / scale + "px";
|
||||
const clipY = elRect.top + intersection[1] - widgetRect.y / scale + "px";
|
||||
const clipWidth = intersection[2] + "px";
|
||||
const clipHeight = intersection[3] + "px";
|
||||
const path = `polygon(0% 0%, 0% 100%, ${clipX} 100%, ${clipX} ${clipY}, calc(${clipX} + ${clipWidth}) ${clipY}, calc(${clipX} + ${clipWidth}) calc(${clipY} + ${clipHeight}), ${clipX} calc(${clipY} + ${clipHeight}), ${clipX} 100%, 100% 100%, 100% 0%)`;
|
||||
return path;
|
||||
const widgetRect = element.getBoundingClientRect()
|
||||
const clipX = elRect.left + intersection[0] - widgetRect.x / scale + 'px'
|
||||
const clipY = elRect.top + intersection[1] - widgetRect.y / scale + 'px'
|
||||
const clipWidth = intersection[2] + 'px'
|
||||
const clipHeight = intersection[3] + 'px'
|
||||
const path = `polygon(0% 0%, 0% 100%, ${clipX} 100%, ${clipX} ${clipY}, calc(${clipX} + ${clipWidth}) ${clipY}, calc(${clipX} + ${clipWidth}) calc(${clipY} + ${clipHeight}), ${clipX} calc(${clipY} + ${clipHeight}), ${clipX} 100%, 100% 100%, 100% 0%)`
|
||||
return path
|
||||
}
|
||||
return "";
|
||||
return ''
|
||||
}
|
||||
|
||||
function computeSize(size: [number, number]): void {
|
||||
if (this.widgets?.[0]?.last_y == null) return;
|
||||
if (this.widgets?.[0]?.last_y == null) return
|
||||
|
||||
let y = this.widgets[0].last_y;
|
||||
let freeSpace = size[1] - y;
|
||||
let y = this.widgets[0].last_y
|
||||
let freeSpace = size[1] - y
|
||||
|
||||
let widgetHeight = 0;
|
||||
let dom = [];
|
||||
let widgetHeight = 0
|
||||
let dom = []
|
||||
for (const w of this.widgets) {
|
||||
if (w.type === "converted-widget") {
|
||||
if (w.type === 'converted-widget') {
|
||||
// Ignore
|
||||
delete w.computedHeight;
|
||||
delete w.computedHeight
|
||||
} else if (w.computeSize) {
|
||||
widgetHeight += w.computeSize()[1] + 4;
|
||||
widgetHeight += w.computeSize()[1] + 4
|
||||
} else if (w.element) {
|
||||
// Extract DOM widget size info
|
||||
const styles = getComputedStyle(w.element);
|
||||
const styles = getComputedStyle(w.element)
|
||||
let minHeight =
|
||||
w.options.getMinHeight?.() ??
|
||||
parseInt(styles.getPropertyValue("--comfy-widget-min-height"));
|
||||
parseInt(styles.getPropertyValue('--comfy-widget-min-height'))
|
||||
let maxHeight =
|
||||
w.options.getMaxHeight?.() ??
|
||||
parseInt(styles.getPropertyValue("--comfy-widget-max-height"));
|
||||
parseInt(styles.getPropertyValue('--comfy-widget-max-height'))
|
||||
|
||||
let prefHeight =
|
||||
w.options.getHeight?.() ??
|
||||
styles.getPropertyValue("--comfy-widget-height");
|
||||
if (prefHeight.endsWith?.("%")) {
|
||||
styles.getPropertyValue('--comfy-widget-height')
|
||||
if (prefHeight.endsWith?.('%')) {
|
||||
prefHeight =
|
||||
size[1] *
|
||||
(parseFloat(prefHeight.substring(0, prefHeight.length - 1)) / 100);
|
||||
(parseFloat(prefHeight.substring(0, prefHeight.length - 1)) / 100)
|
||||
} else {
|
||||
prefHeight = parseInt(prefHeight);
|
||||
prefHeight = parseInt(prefHeight)
|
||||
if (isNaN(minHeight)) {
|
||||
minHeight = prefHeight;
|
||||
minHeight = prefHeight
|
||||
}
|
||||
}
|
||||
if (isNaN(minHeight)) {
|
||||
minHeight = 50;
|
||||
minHeight = 50
|
||||
}
|
||||
if (!isNaN(maxHeight)) {
|
||||
if (!isNaN(prefHeight)) {
|
||||
prefHeight = Math.min(prefHeight, maxHeight);
|
||||
prefHeight = Math.min(prefHeight, maxHeight)
|
||||
} else {
|
||||
prefHeight = maxHeight;
|
||||
prefHeight = maxHeight
|
||||
}
|
||||
}
|
||||
dom.push({
|
||||
minHeight,
|
||||
prefHeight,
|
||||
w,
|
||||
});
|
||||
w
|
||||
})
|
||||
} else {
|
||||
widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4;
|
||||
widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4
|
||||
}
|
||||
}
|
||||
|
||||
freeSpace -= widgetHeight;
|
||||
freeSpace -= widgetHeight
|
||||
|
||||
// Calculate sizes with all widgets at their min height
|
||||
const prefGrow = []; // Nodes that want to grow to their prefd size
|
||||
const canGrow = []; // Nodes that can grow to auto size
|
||||
let growBy = 0;
|
||||
const prefGrow = [] // Nodes that want to grow to their prefd size
|
||||
const canGrow = [] // Nodes that can grow to auto size
|
||||
let growBy = 0
|
||||
for (const d of dom) {
|
||||
freeSpace -= d.minHeight;
|
||||
freeSpace -= d.minHeight
|
||||
if (isNaN(d.prefHeight)) {
|
||||
canGrow.push(d);
|
||||
d.w.computedHeight = d.minHeight;
|
||||
canGrow.push(d)
|
||||
d.w.computedHeight = d.minHeight
|
||||
} else {
|
||||
const diff = d.prefHeight - d.minHeight;
|
||||
const diff = d.prefHeight - d.minHeight
|
||||
if (diff > 0) {
|
||||
prefGrow.push(d);
|
||||
growBy += diff;
|
||||
d.diff = diff;
|
||||
prefGrow.push(d)
|
||||
growBy += diff
|
||||
d.diff = diff
|
||||
} else {
|
||||
d.w.computedHeight = d.minHeight;
|
||||
d.w.computedHeight = d.minHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.imgs && !this.widgets.find((w) => w.name === ANIM_PREVIEW_WIDGET)) {
|
||||
// Allocate space for image
|
||||
freeSpace -= 220;
|
||||
freeSpace -= 220
|
||||
}
|
||||
|
||||
this.freeWidgetSpace = freeSpace;
|
||||
this.freeWidgetSpace = freeSpace
|
||||
|
||||
if (freeSpace < 0) {
|
||||
// Not enough space for all widgets so we need to grow
|
||||
size[1] -= freeSpace;
|
||||
this.graph.setDirtyCanvas(true);
|
||||
size[1] -= freeSpace
|
||||
this.graph.setDirtyCanvas(true)
|
||||
} else {
|
||||
// Share the space between each
|
||||
const growDiff = freeSpace - growBy;
|
||||
const growDiff = freeSpace - growBy
|
||||
if (growDiff > 0) {
|
||||
// All pref sizes can be fulfilled
|
||||
freeSpace = growDiff;
|
||||
freeSpace = growDiff
|
||||
for (const d of prefGrow) {
|
||||
d.w.computedHeight = d.prefHeight;
|
||||
d.w.computedHeight = d.prefHeight
|
||||
}
|
||||
} else {
|
||||
// We need to grow evenly
|
||||
const shared = -growDiff / prefGrow.length;
|
||||
const shared = -growDiff / prefGrow.length
|
||||
for (const d of prefGrow) {
|
||||
d.w.computedHeight = d.prefHeight - shared;
|
||||
d.w.computedHeight = d.prefHeight - shared
|
||||
}
|
||||
freeSpace = 0;
|
||||
freeSpace = 0
|
||||
}
|
||||
|
||||
if (freeSpace > 0 && canGrow.length) {
|
||||
// Grow any that are auto height
|
||||
const shared = freeSpace / canGrow.length;
|
||||
const shared = freeSpace / canGrow.length
|
||||
for (const d of canGrow) {
|
||||
d.w.computedHeight += shared;
|
||||
d.w.computedHeight += shared
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Position each of the widgets
|
||||
for (const w of this.widgets) {
|
||||
w.y = y;
|
||||
w.y = y
|
||||
if (w.computedHeight) {
|
||||
y += w.computedHeight;
|
||||
y += w.computedHeight
|
||||
} else if (w.computeSize) {
|
||||
y += w.computeSize()[1] + 4;
|
||||
y += w.computeSize()[1] + 4
|
||||
} else {
|
||||
y += LiteGraph.NODE_WIDGET_HEIGHT + 4;
|
||||
y += LiteGraph.NODE_WIDGET_HEIGHT + 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Override the compute visible nodes function to allow us to hide/show DOM elements when the node goes offscreen
|
||||
const elementWidgets = new Set();
|
||||
const elementWidgets = new Set()
|
||||
//@ts-ignore
|
||||
const computeVisibleNodes = LGraphCanvas.prototype.computeVisibleNodes;
|
||||
const computeVisibleNodes = LGraphCanvas.prototype.computeVisibleNodes
|
||||
//@ts-ignore
|
||||
LGraphCanvas.prototype.computeVisibleNodes = function (): LGraphNode[] {
|
||||
const visibleNodes = computeVisibleNodes.apply(this, arguments);
|
||||
const visibleNodes = computeVisibleNodes.apply(this, arguments)
|
||||
// @ts-ignore
|
||||
for (const node of app.graph._nodes) {
|
||||
if (elementWidgets.has(node)) {
|
||||
const hidden = visibleNodes.indexOf(node) === -1;
|
||||
const hidden = visibleNodes.indexOf(node) === -1
|
||||
for (const w of node.widgets) {
|
||||
// @ts-ignore
|
||||
if (w.element) {
|
||||
// @ts-ignore
|
||||
w.element.hidden = hidden;
|
||||
w.element.hidden = hidden
|
||||
// @ts-ignore
|
||||
w.element.style.display = hidden ? "none" : undefined;
|
||||
w.element.style.display = hidden ? 'none' : undefined
|
||||
if (hidden) {
|
||||
w.options.onHide?.(w);
|
||||
w.options.onHide?.(w)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return visibleNodes;
|
||||
};
|
||||
return visibleNodes
|
||||
}
|
||||
|
||||
let enableDomClipping = true;
|
||||
let enableDomClipping = true
|
||||
|
||||
export function addDomClippingSetting(): void {
|
||||
app.ui.settings.addSetting({
|
||||
id: "Comfy.DOMClippingEnabled",
|
||||
name: "Enable DOM element clipping (enabling may reduce performance)",
|
||||
type: "boolean",
|
||||
id: 'Comfy.DOMClippingEnabled',
|
||||
name: 'Enable DOM element clipping (enabling may reduce performance)',
|
||||
type: 'boolean',
|
||||
defaultValue: enableDomClipping,
|
||||
onChange(value) {
|
||||
enableDomClipping = !!value;
|
||||
},
|
||||
});
|
||||
enableDomClipping = !!value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
@@ -264,33 +264,33 @@ LGraphNode.prototype.addDOMWidget = function (
|
||||
element: HTMLElement,
|
||||
options: Record<string, any>
|
||||
): DOMWidget {
|
||||
options = { hideOnZoom: true, selectOn: ["focus", "click"], ...options };
|
||||
options = { hideOnZoom: true, selectOn: ['focus', 'click'], ...options }
|
||||
|
||||
if (!element.parentElement) {
|
||||
document.body.append(element);
|
||||
document.body.append(element)
|
||||
}
|
||||
element.hidden = true;
|
||||
element.style.display = "none";
|
||||
element.hidden = true
|
||||
element.style.display = 'none'
|
||||
|
||||
let mouseDownHandler;
|
||||
let mouseDownHandler
|
||||
if (element.blur) {
|
||||
mouseDownHandler = (event) => {
|
||||
if (!element.contains(event.target)) {
|
||||
element.blur();
|
||||
element.blur()
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", mouseDownHandler);
|
||||
}
|
||||
document.addEventListener('mousedown', mouseDownHandler)
|
||||
}
|
||||
|
||||
const widget: DOMWidget = {
|
||||
type,
|
||||
name,
|
||||
get value() {
|
||||
return options.getValue?.() ?? undefined;
|
||||
return options.getValue?.() ?? undefined
|
||||
},
|
||||
set value(v) {
|
||||
options.setValue?.(v);
|
||||
widget.callback?.(widget.value);
|
||||
options.setValue?.(v)
|
||||
widget.callback?.(widget.value)
|
||||
},
|
||||
draw: function (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
@@ -300,99 +300,99 @@ LGraphNode.prototype.addDOMWidget = function (
|
||||
widgetHeight: number
|
||||
) {
|
||||
if (widget.computedHeight == null) {
|
||||
computeSize.call(node, node.size);
|
||||
computeSize.call(node, node.size)
|
||||
}
|
||||
|
||||
const hidden =
|
||||
node.flags?.collapsed ||
|
||||
(!!options.hideOnZoom && app.canvas.ds.scale < 0.5) ||
|
||||
widget.computedHeight <= 0 ||
|
||||
widget.type === "converted-widget" ||
|
||||
widget.type === "hidden";
|
||||
element.hidden = hidden;
|
||||
element.style.display = hidden ? "none" : null;
|
||||
widget.type === 'converted-widget' ||
|
||||
widget.type === 'hidden'
|
||||
element.hidden = hidden
|
||||
element.style.display = hidden ? 'none' : null
|
||||
if (hidden) {
|
||||
widget.options.onHide?.(widget);
|
||||
return;
|
||||
widget.options.onHide?.(widget)
|
||||
return
|
||||
}
|
||||
|
||||
const margin = 10;
|
||||
const elRect = ctx.canvas.getBoundingClientRect();
|
||||
const margin = 10
|
||||
const elRect = ctx.canvas.getBoundingClientRect()
|
||||
const transform = new DOMMatrix()
|
||||
.scaleSelf(
|
||||
elRect.width / ctx.canvas.width,
|
||||
elRect.height / ctx.canvas.height
|
||||
)
|
||||
.multiplySelf(ctx.getTransform())
|
||||
.translateSelf(margin, margin + y);
|
||||
.translateSelf(margin, margin + y)
|
||||
|
||||
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d);
|
||||
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d)
|
||||
|
||||
Object.assign(element.style, {
|
||||
transformOrigin: "0 0",
|
||||
transformOrigin: '0 0',
|
||||
transform: scale,
|
||||
left: `${transform.a + transform.e + elRect.left}px`,
|
||||
top: `${transform.d + transform.f + elRect.top}px`,
|
||||
width: `${widgetWidth - margin * 2}px`,
|
||||
height: `${(widget.computedHeight ?? 50) - margin * 2}px`,
|
||||
position: "absolute",
|
||||
position: 'absolute',
|
||||
// @ts-ignore
|
||||
zIndex: app.graph._nodes.indexOf(node),
|
||||
});
|
||||
zIndex: app.graph._nodes.indexOf(node)
|
||||
})
|
||||
|
||||
if (enableDomClipping) {
|
||||
element.style.clipPath = getClipPath(node, element);
|
||||
element.style.willChange = "clip-path";
|
||||
element.style.clipPath = getClipPath(node, element)
|
||||
element.style.willChange = 'clip-path'
|
||||
}
|
||||
|
||||
this.options.onDraw?.(widget);
|
||||
this.options.onDraw?.(widget)
|
||||
},
|
||||
element,
|
||||
options,
|
||||
onRemove() {
|
||||
if (mouseDownHandler) {
|
||||
document.removeEventListener("mousedown", mouseDownHandler);
|
||||
document.removeEventListener('mousedown', mouseDownHandler)
|
||||
}
|
||||
element.remove()
|
||||
}
|
||||
}
|
||||
element.remove();
|
||||
},
|
||||
};
|
||||
|
||||
for (const evt of options.selectOn) {
|
||||
element.addEventListener(evt, () => {
|
||||
app.canvas.selectNode(this);
|
||||
app.canvas.bringToFront(this);
|
||||
});
|
||||
app.canvas.selectNode(this)
|
||||
app.canvas.bringToFront(this)
|
||||
})
|
||||
}
|
||||
|
||||
this.addCustomWidget(widget);
|
||||
elementWidgets.add(this);
|
||||
this.addCustomWidget(widget)
|
||||
elementWidgets.add(this)
|
||||
|
||||
const collapse = this.collapse;
|
||||
const collapse = this.collapse
|
||||
this.collapse = function () {
|
||||
collapse.apply(this, arguments);
|
||||
collapse.apply(this, arguments)
|
||||
if (this.flags?.collapsed) {
|
||||
element.hidden = true;
|
||||
element.style.display = "none";
|
||||
element.hidden = true
|
||||
element.style.display = 'none'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onRemoved = this.onRemoved;
|
||||
const onRemoved = this.onRemoved
|
||||
this.onRemoved = function () {
|
||||
element.remove();
|
||||
elementWidgets.delete(this);
|
||||
onRemoved?.apply(this, arguments);
|
||||
};
|
||||
element.remove()
|
||||
elementWidgets.delete(this)
|
||||
onRemoved?.apply(this, arguments)
|
||||
}
|
||||
|
||||
if (!this[SIZE]) {
|
||||
this[SIZE] = true;
|
||||
const onResize = this.onResize;
|
||||
this[SIZE] = true
|
||||
const onResize = this.onResize
|
||||
this.onResize = function (size) {
|
||||
options.beforeResize?.call(widget, this);
|
||||
computeSize.call(this, size);
|
||||
onResize?.apply(this, arguments);
|
||||
options.afterResize?.call(widget, this);
|
||||
};
|
||||
options.beforeResize?.call(widget, this)
|
||||
computeSize.call(this, size)
|
||||
onResize?.apply(this, arguments)
|
||||
options.afterResize?.call(widget, this)
|
||||
}
|
||||
}
|
||||
|
||||
return widget;
|
||||
};
|
||||
return widget
|
||||
}
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
import { useWorkspaceStore } from "@/stores/workspaceStateStore";
|
||||
import { ExtensionManager, SidebarTabExtension } from "@/types/extensionTypes";
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
|
||||
import { ExtensionManager, SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
export class ExtensionManagerImpl implements ExtensionManager {
|
||||
private sidebarTabs: SidebarTabExtension[] = [];
|
||||
private workspaceStore = useWorkspaceStore();
|
||||
private sidebarTabs: SidebarTabExtension[] = []
|
||||
private workspaceStore = useWorkspaceStore()
|
||||
|
||||
registerSidebarTab(tab: SidebarTabExtension) {
|
||||
this.sidebarTabs.push(tab);
|
||||
this.updateSidebarOrder();
|
||||
this.sidebarTabs.push(tab)
|
||||
this.updateSidebarOrder()
|
||||
}
|
||||
|
||||
unregisterSidebarTab(id: string) {
|
||||
const index = this.sidebarTabs.findIndex((tab) => tab.id === id);
|
||||
const index = this.sidebarTabs.findIndex((tab) => tab.id === id)
|
||||
if (index !== -1) {
|
||||
const tab = this.sidebarTabs[index];
|
||||
if (tab.type === "custom" && tab.destroy) {
|
||||
tab.destroy();
|
||||
const tab = this.sidebarTabs[index]
|
||||
if (tab.type === 'custom' && tab.destroy) {
|
||||
tab.destroy()
|
||||
}
|
||||
this.sidebarTabs.splice(index, 1);
|
||||
this.updateSidebarOrder();
|
||||
this.sidebarTabs.splice(index, 1)
|
||||
this.updateSidebarOrder()
|
||||
}
|
||||
}
|
||||
|
||||
getSidebarTabs() {
|
||||
return this.sidebarTabs.sort((a, b) => {
|
||||
const orderA = this.workspaceStore.sidebarTabsOrder.indexOf(a.id);
|
||||
const orderB = this.workspaceStore.sidebarTabsOrder.indexOf(b.id);
|
||||
return orderA - orderB;
|
||||
});
|
||||
const orderA = this.workspaceStore.sidebarTabsOrder.indexOf(a.id)
|
||||
const orderB = this.workspaceStore.sidebarTabsOrder.indexOf(b.id)
|
||||
return orderA - orderB
|
||||
})
|
||||
}
|
||||
|
||||
private updateSidebarOrder() {
|
||||
const currentOrder = this.workspaceStore.sidebarTabsOrder;
|
||||
const currentOrder = this.workspaceStore.sidebarTabsOrder
|
||||
const newTabs = this.sidebarTabs.filter(
|
||||
(tab) => !currentOrder.includes(tab.id)
|
||||
);
|
||||
)
|
||||
this.workspaceStore.updateSidebarOrder([
|
||||
...currentOrder,
|
||||
...newTabs.map((tab) => tab.id),
|
||||
]);
|
||||
...newTabs.map((tab) => tab.id)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { $el, ComfyDialog } from "./ui";
|
||||
import { api } from "./api";
|
||||
import type { ComfyApp } from "./app";
|
||||
import { $el, ComfyDialog } from './ui'
|
||||
import { api } from './api'
|
||||
import type { ComfyApp } from './app'
|
||||
|
||||
$el("style", {
|
||||
$el('style', {
|
||||
textContent: `
|
||||
.comfy-logging-logs {
|
||||
display: grid;
|
||||
@@ -23,17 +23,17 @@ $el("style", {
|
||||
padding: 5px;
|
||||
}
|
||||
`,
|
||||
parent: document.body,
|
||||
});
|
||||
parent: document.body
|
||||
})
|
||||
|
||||
// Stringify function supporting max depth and removal of circular references
|
||||
// https://stackoverflow.com/a/57193345
|
||||
function stringify(val, depth, replacer, space, onGetObjID?) {
|
||||
depth = isNaN(+depth) ? 1 : depth;
|
||||
var recursMap = new WeakMap();
|
||||
depth = isNaN(+depth) ? 1 : depth
|
||||
var recursMap = new WeakMap()
|
||||
function _build(val, depth, o?, a?, r?) {
|
||||
// (JSON.stringify() has it's own rules, which we respect here by using it for property iteration)
|
||||
return !val || typeof val != "object"
|
||||
return !val || typeof val != 'object'
|
||||
? val
|
||||
: ((r = recursMap.has(val)),
|
||||
recursMap.set(val, true),
|
||||
@@ -42,201 +42,201 @@ function stringify(val, depth, replacer, space, onGetObjID?) {
|
||||
? (o = (onGetObjID && onGetObjID(val)) || null)
|
||||
: JSON.stringify(val, function (k, v) {
|
||||
if (a || depth > 0) {
|
||||
if (replacer) v = replacer(k, v);
|
||||
if (!k) return (a = Array.isArray(v)), (val = v);
|
||||
!o && (o = a ? [] : {});
|
||||
o[k] = _build(v, a ? depth : depth - 1);
|
||||
if (replacer) v = replacer(k, v)
|
||||
if (!k) return (a = Array.isArray(v)), (val = v)
|
||||
!o && (o = a ? [] : {})
|
||||
o[k] = _build(v, a ? depth : depth - 1)
|
||||
}
|
||||
}),
|
||||
o === void 0 ? (a ? [] : {}) : o);
|
||||
o === void 0 ? (a ? [] : {}) : o)
|
||||
}
|
||||
return JSON.stringify(_build(val, depth), null, space);
|
||||
return JSON.stringify(_build(val, depth), null, space)
|
||||
}
|
||||
|
||||
const jsonReplacer = (k, v, ui) => {
|
||||
if (v instanceof Array && v.length === 1) {
|
||||
v = v[0];
|
||||
v = v[0]
|
||||
}
|
||||
if (v instanceof Date) {
|
||||
v = v.toISOString();
|
||||
v = v.toISOString()
|
||||
if (ui) {
|
||||
v = v.split("T")[1];
|
||||
v = v.split('T')[1]
|
||||
}
|
||||
}
|
||||
if (v instanceof Error) {
|
||||
let err = "";
|
||||
if (v.name) err += v.name + "\n";
|
||||
if (v.message) err += v.message + "\n";
|
||||
if (v.stack) err += v.stack + "\n";
|
||||
let err = ''
|
||||
if (v.name) err += v.name + '\n'
|
||||
if (v.message) err += v.message + '\n'
|
||||
if (v.stack) err += v.stack + '\n'
|
||||
if (!err) {
|
||||
err = v.toString();
|
||||
err = v.toString()
|
||||
}
|
||||
v = err;
|
||||
v = err
|
||||
}
|
||||
return v;
|
||||
};
|
||||
return v
|
||||
}
|
||||
|
||||
const fileInput: HTMLInputElement = $el("input", {
|
||||
type: "file",
|
||||
accept: ".json",
|
||||
style: { display: "none" },
|
||||
parent: document.body,
|
||||
}) as HTMLInputElement;
|
||||
const fileInput: HTMLInputElement = $el('input', {
|
||||
type: 'file',
|
||||
accept: '.json',
|
||||
style: { display: 'none' },
|
||||
parent: document.body
|
||||
}) as HTMLInputElement
|
||||
|
||||
class ComfyLoggingDialog extends ComfyDialog {
|
||||
logging: any;
|
||||
logging: any
|
||||
|
||||
constructor(logging) {
|
||||
super();
|
||||
this.logging = logging;
|
||||
super()
|
||||
this.logging = logging
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.logging.clear();
|
||||
this.show();
|
||||
this.logging.clear()
|
||||
this.show()
|
||||
}
|
||||
|
||||
export() {
|
||||
const blob = new Blob(
|
||||
[stringify([...this.logging.entries], 20, jsonReplacer, "\t")],
|
||||
[stringify([...this.logging.entries], 20, jsonReplacer, '\t')],
|
||||
{
|
||||
type: "application/json",
|
||||
type: 'application/json'
|
||||
}
|
||||
);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = $el("a", {
|
||||
)
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = $el('a', {
|
||||
href: url,
|
||||
download: `comfyui-logs-${Date.now()}.json`,
|
||||
style: { display: "none" },
|
||||
parent: document.body,
|
||||
});
|
||||
a.click();
|
||||
style: { display: 'none' },
|
||||
parent: document.body
|
||||
})
|
||||
a.click()
|
||||
setTimeout(function () {
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
a.remove()
|
||||
window.URL.revokeObjectURL(url)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
import() {
|
||||
fileInput.onchange = () => {
|
||||
const reader = new FileReader();
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
fileInput.remove();
|
||||
fileInput.remove()
|
||||
try {
|
||||
const obj = JSON.parse(reader.result as string);
|
||||
const obj = JSON.parse(reader.result as string)
|
||||
if (obj instanceof Array) {
|
||||
this.show(obj);
|
||||
this.show(obj)
|
||||
} else {
|
||||
throw new Error("Invalid file selected.");
|
||||
throw new Error('Invalid file selected.')
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Unable to load logs: " + error.message);
|
||||
alert('Unable to load logs: ' + error.message)
|
||||
}
|
||||
};
|
||||
reader.readAsText(fileInput.files[0]);
|
||||
};
|
||||
fileInput.click();
|
||||
}
|
||||
reader.readAsText(fileInput.files[0])
|
||||
}
|
||||
fileInput.click()
|
||||
}
|
||||
|
||||
createButtons() {
|
||||
return [
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Clear",
|
||||
onclick: () => this.clear(),
|
||||
$el('button', {
|
||||
type: 'button',
|
||||
textContent: 'Clear',
|
||||
onclick: () => this.clear()
|
||||
}),
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Export logs...",
|
||||
onclick: () => this.export(),
|
||||
$el('button', {
|
||||
type: 'button',
|
||||
textContent: 'Export logs...',
|
||||
onclick: () => this.export()
|
||||
}),
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "View exported logs...",
|
||||
onclick: () => this.import(),
|
||||
$el('button', {
|
||||
type: 'button',
|
||||
textContent: 'View exported logs...',
|
||||
onclick: () => this.import()
|
||||
}),
|
||||
...super.createButtons(),
|
||||
];
|
||||
...super.createButtons()
|
||||
]
|
||||
}
|
||||
|
||||
getTypeColor(type) {
|
||||
switch (type) {
|
||||
case "error":
|
||||
return "red";
|
||||
case "warn":
|
||||
return "orange";
|
||||
case "debug":
|
||||
return "dodgerblue";
|
||||
case 'error':
|
||||
return 'red'
|
||||
case 'warn':
|
||||
return 'orange'
|
||||
case 'debug':
|
||||
return 'dodgerblue'
|
||||
}
|
||||
}
|
||||
|
||||
show(entries?: any[]) {
|
||||
if (!entries) entries = this.logging.entries;
|
||||
this.element.style.width = "100%";
|
||||
if (!entries) entries = this.logging.entries
|
||||
this.element.style.width = '100%'
|
||||
const cols = {
|
||||
source: "Source",
|
||||
type: "Type",
|
||||
timestamp: "Timestamp",
|
||||
message: "Message",
|
||||
};
|
||||
const keys = Object.keys(cols);
|
||||
source: 'Source',
|
||||
type: 'Type',
|
||||
timestamp: 'Timestamp',
|
||||
message: 'Message'
|
||||
}
|
||||
const keys = Object.keys(cols)
|
||||
const headers = Object.values(cols).map((title) =>
|
||||
$el("div.comfy-logging-title", {
|
||||
textContent: title,
|
||||
$el('div.comfy-logging-title', {
|
||||
textContent: title
|
||||
})
|
||||
);
|
||||
)
|
||||
const rows = entries.map((entry, i) => {
|
||||
return $el(
|
||||
"div.comfy-logging-log",
|
||||
'div.comfy-logging-log',
|
||||
{
|
||||
$: (el) =>
|
||||
el.style.setProperty(
|
||||
"--row-bg",
|
||||
`var(--tr-${i % 2 ? "even" : "odd"}-bg-color)`
|
||||
),
|
||||
'--row-bg',
|
||||
`var(--tr-${i % 2 ? 'even' : 'odd'}-bg-color)`
|
||||
)
|
||||
},
|
||||
keys.map((key) => {
|
||||
let v = entry[key];
|
||||
let color;
|
||||
if (key === "type") {
|
||||
color = this.getTypeColor(v);
|
||||
let v = entry[key]
|
||||
let color
|
||||
if (key === 'type') {
|
||||
color = this.getTypeColor(v)
|
||||
} else {
|
||||
v = jsonReplacer(key, v, true);
|
||||
v = jsonReplacer(key, v, true)
|
||||
|
||||
if (typeof v === "object") {
|
||||
v = stringify(v, 5, jsonReplacer, " ");
|
||||
if (typeof v === 'object') {
|
||||
v = stringify(v, 5, jsonReplacer, ' ')
|
||||
}
|
||||
}
|
||||
|
||||
return $el("div", {
|
||||
return $el('div', {
|
||||
style: {
|
||||
color,
|
||||
color
|
||||
},
|
||||
textContent: v,
|
||||
});
|
||||
textContent: v
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const grid = $el(
|
||||
"div.comfy-logging-logs",
|
||||
'div.comfy-logging-logs',
|
||||
{
|
||||
style: {
|
||||
gridTemplateColumns: `repeat(${headers.length}, 1fr)`,
|
||||
},
|
||||
gridTemplateColumns: `repeat(${headers.length}, 1fr)`
|
||||
}
|
||||
},
|
||||
[...headers, ...rows]
|
||||
);
|
||||
const els = [grid];
|
||||
)
|
||||
const els = [grid]
|
||||
if (!this.logging.enabled) {
|
||||
els.unshift(
|
||||
$el("h3", {
|
||||
style: { textAlign: "center" },
|
||||
textContent: "Logging is disabled",
|
||||
$el('h3', {
|
||||
style: { textAlign: 'center' },
|
||||
textContent: 'Logging is disabled'
|
||||
})
|
||||
);
|
||||
)
|
||||
}
|
||||
super.show($el("div", els));
|
||||
super.show($el('div', els))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,118 +244,118 @@ export class ComfyLogging {
|
||||
/**
|
||||
* @type Array<{ source: string, type: string, timestamp: Date, message: any }>
|
||||
*/
|
||||
entries = [];
|
||||
entries = []
|
||||
|
||||
#enabled;
|
||||
#console = {};
|
||||
#enabled
|
||||
#console = {}
|
||||
|
||||
app: ComfyApp;
|
||||
dialog: ComfyLoggingDialog;
|
||||
app: ComfyApp
|
||||
dialog: ComfyLoggingDialog
|
||||
|
||||
get enabled() {
|
||||
return this.#enabled;
|
||||
return this.#enabled
|
||||
}
|
||||
|
||||
set enabled(value) {
|
||||
if (value === this.#enabled) return;
|
||||
if (value === this.#enabled) return
|
||||
if (value) {
|
||||
this.patchConsole();
|
||||
this.patchConsole()
|
||||
} else {
|
||||
this.unpatchConsole();
|
||||
this.unpatchConsole()
|
||||
}
|
||||
this.#enabled = value;
|
||||
this.#enabled = value
|
||||
}
|
||||
|
||||
constructor(app) {
|
||||
this.app = app;
|
||||
this.app = app
|
||||
|
||||
this.dialog = new ComfyLoggingDialog(this);
|
||||
this.addSetting();
|
||||
this.catchUnhandled();
|
||||
this.addInitData();
|
||||
this.dialog = new ComfyLoggingDialog(this)
|
||||
this.addSetting()
|
||||
this.catchUnhandled()
|
||||
this.addInitData()
|
||||
}
|
||||
|
||||
addSetting() {
|
||||
const settingId: string = "Comfy.Logging.Enabled";
|
||||
const htmlSettingId = settingId.replaceAll(".", "-");
|
||||
const settingId: string = 'Comfy.Logging.Enabled'
|
||||
const htmlSettingId = settingId.replaceAll('.', '-')
|
||||
const setting = this.app.ui.settings.addSetting({
|
||||
id: settingId,
|
||||
name: settingId,
|
||||
defaultValue: true,
|
||||
onChange: (value) => {
|
||||
this.enabled = value;
|
||||
this.enabled = value
|
||||
},
|
||||
type: (name, setter, value) => {
|
||||
return $el("tr", [
|
||||
$el("td", [
|
||||
$el("label", {
|
||||
textContent: "Logging",
|
||||
for: htmlSettingId,
|
||||
}),
|
||||
return $el('tr', [
|
||||
$el('td', [
|
||||
$el('label', {
|
||||
textContent: 'Logging',
|
||||
for: htmlSettingId
|
||||
})
|
||||
]),
|
||||
$el("td", [
|
||||
$el("input", {
|
||||
$el('td', [
|
||||
$el('input', {
|
||||
id: htmlSettingId,
|
||||
type: "checkbox",
|
||||
type: 'checkbox',
|
||||
checked: value,
|
||||
onchange: (event) => {
|
||||
setter(event.target.checked);
|
||||
},
|
||||
setter(event.target.checked)
|
||||
}
|
||||
}),
|
||||
$el("button", {
|
||||
textContent: "View Logs",
|
||||
$el('button', {
|
||||
textContent: 'View Logs',
|
||||
onclick: () => {
|
||||
this.app.ui.settings.element.close();
|
||||
this.dialog.show();
|
||||
this.app.ui.settings.element.close()
|
||||
this.dialog.show()
|
||||
},
|
||||
style: {
|
||||
fontSize: "14px",
|
||||
display: "block",
|
||||
marginTop: "5px",
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
},
|
||||
});
|
||||
this.enabled = setting.value;
|
||||
fontSize: '14px',
|
||||
display: 'block',
|
||||
marginTop: '5px'
|
||||
}
|
||||
})
|
||||
])
|
||||
])
|
||||
}
|
||||
})
|
||||
this.enabled = setting.value
|
||||
}
|
||||
|
||||
patchConsole() {
|
||||
// Capture common console outputs
|
||||
const self = this;
|
||||
for (const type of ["log", "warn", "error", "debug"]) {
|
||||
const orig = console[type];
|
||||
this.#console[type] = orig;
|
||||
const self = this
|
||||
for (const type of ['log', 'warn', 'error', 'debug']) {
|
||||
const orig = console[type]
|
||||
this.#console[type] = orig
|
||||
console[type] = function () {
|
||||
orig.apply(console, arguments);
|
||||
self.addEntry("console", type, ...arguments);
|
||||
};
|
||||
orig.apply(console, arguments)
|
||||
self.addEntry('console', type, ...arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unpatchConsole() {
|
||||
// Restore original console functions
|
||||
for (const type of Object.keys(this.#console)) {
|
||||
console[type] = this.#console[type];
|
||||
console[type] = this.#console[type]
|
||||
}
|
||||
this.#console = {};
|
||||
this.#console = {}
|
||||
}
|
||||
|
||||
catchUnhandled() {
|
||||
// Capture uncaught errors
|
||||
window.addEventListener("error", (e) => {
|
||||
this.addEntry("window", "error", e.error ?? "Unknown error");
|
||||
return false;
|
||||
});
|
||||
window.addEventListener('error', (e) => {
|
||||
this.addEntry('window', 'error', e.error ?? 'Unknown error')
|
||||
return false
|
||||
})
|
||||
|
||||
window.addEventListener("unhandledrejection", (e) => {
|
||||
this.addEntry("unhandledrejection", "error", e.reason ?? "Unknown error");
|
||||
});
|
||||
window.addEventListener('unhandledrejection', (e) => {
|
||||
this.addEntry('unhandledrejection', 'error', e.reason ?? 'Unknown error')
|
||||
})
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.entries = [];
|
||||
this.entries = []
|
||||
}
|
||||
|
||||
addEntry(source, type, ...args) {
|
||||
@@ -364,20 +364,20 @@ export class ComfyLogging {
|
||||
source,
|
||||
type,
|
||||
timestamp: new Date(),
|
||||
message: args,
|
||||
});
|
||||
message: args
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
log(source, ...args) {
|
||||
this.addEntry(source, "log", ...args);
|
||||
this.addEntry(source, 'log', ...args)
|
||||
}
|
||||
|
||||
async addInitData() {
|
||||
if (!this.enabled) return;
|
||||
const source = "ComfyUI.Logging";
|
||||
this.addEntry(source, "debug", { UserAgent: navigator.userAgent });
|
||||
const systemStats = await api.getSystemStats();
|
||||
this.addEntry(source, "debug", systemStats);
|
||||
if (!this.enabled) return
|
||||
const source = 'ComfyUI.Logging'
|
||||
this.addEntry(source, 'debug', { UserAgent: navigator.userAgent })
|
||||
const systemStats = await api.getSystemStats()
|
||||
this.addEntry(source, 'debug', systemStats)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +1,76 @@
|
||||
export function getFromFlacBuffer(buffer: ArrayBuffer): Record<string, string> {
|
||||
const dataView = new DataView(buffer);
|
||||
const dataView = new DataView(buffer)
|
||||
|
||||
// Verify the FLAC signature
|
||||
const signature = String.fromCharCode(...new Uint8Array(buffer, 0, 4));
|
||||
if (signature !== "fLaC") {
|
||||
console.error("Not a valid FLAC file");
|
||||
return;
|
||||
const signature = String.fromCharCode(...new Uint8Array(buffer, 0, 4))
|
||||
if (signature !== 'fLaC') {
|
||||
console.error('Not a valid FLAC file')
|
||||
return
|
||||
}
|
||||
|
||||
// Parse metadata blocks
|
||||
let offset = 4;
|
||||
let vorbisComment = null;
|
||||
let offset = 4
|
||||
let vorbisComment = null
|
||||
while (offset < dataView.byteLength) {
|
||||
const isLastBlock = dataView.getUint8(offset) & 0x80;
|
||||
const blockType = dataView.getUint8(offset) & 0x7f;
|
||||
const blockSize = dataView.getUint32(offset, false) & 0xffffff;
|
||||
offset += 4;
|
||||
const isLastBlock = dataView.getUint8(offset) & 0x80
|
||||
const blockType = dataView.getUint8(offset) & 0x7f
|
||||
const blockSize = dataView.getUint32(offset, false) & 0xffffff
|
||||
offset += 4
|
||||
|
||||
if (blockType === 4) {
|
||||
// Vorbis Comment block type
|
||||
vorbisComment = parseVorbisComment(
|
||||
new DataView(buffer, offset, blockSize)
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
offset += blockSize;
|
||||
if (isLastBlock) break;
|
||||
offset += blockSize
|
||||
if (isLastBlock) break
|
||||
}
|
||||
|
||||
return vorbisComment;
|
||||
return vorbisComment
|
||||
}
|
||||
|
||||
export function getFromFlacFile(file: File): Promise<Record<string, string>> {
|
||||
return new Promise((r) => {
|
||||
const reader = new FileReader();
|
||||
const reader = new FileReader()
|
||||
reader.onload = function (event) {
|
||||
const arrayBuffer = event.target.result as ArrayBuffer;
|
||||
r(getFromFlacBuffer(arrayBuffer));
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
const arrayBuffer = event.target.result as ArrayBuffer
|
||||
r(getFromFlacBuffer(arrayBuffer))
|
||||
}
|
||||
reader.readAsArrayBuffer(file)
|
||||
})
|
||||
}
|
||||
|
||||
// Function to parse the Vorbis Comment block
|
||||
function parseVorbisComment(dataView: DataView): Record<string, string> {
|
||||
let offset = 0;
|
||||
const vendorLength = dataView.getUint32(offset, true);
|
||||
offset += 4;
|
||||
const vendorString = getString(dataView, offset, vendorLength);
|
||||
offset += vendorLength;
|
||||
let offset = 0
|
||||
const vendorLength = dataView.getUint32(offset, true)
|
||||
offset += 4
|
||||
const vendorString = getString(dataView, offset, vendorLength)
|
||||
offset += vendorLength
|
||||
|
||||
const userCommentListLength = dataView.getUint32(offset, true);
|
||||
offset += 4;
|
||||
const comments = {};
|
||||
const userCommentListLength = dataView.getUint32(offset, true)
|
||||
offset += 4
|
||||
const comments = {}
|
||||
for (let i = 0; i < userCommentListLength; i++) {
|
||||
const commentLength = dataView.getUint32(offset, true);
|
||||
offset += 4;
|
||||
const comment = getString(dataView, offset, commentLength);
|
||||
offset += commentLength;
|
||||
const commentLength = dataView.getUint32(offset, true)
|
||||
offset += 4
|
||||
const comment = getString(dataView, offset, commentLength)
|
||||
offset += commentLength
|
||||
|
||||
const [key, value] = comment.split("=");
|
||||
const [key, value] = comment.split('=')
|
||||
|
||||
comments[key] = value;
|
||||
comments[key] = value
|
||||
}
|
||||
|
||||
return comments;
|
||||
return comments
|
||||
}
|
||||
|
||||
function getString(dataView: DataView, offset: number, length: number): string {
|
||||
let string = "";
|
||||
let string = ''
|
||||
for (let i = 0; i < length; i++) {
|
||||
string += String.fromCharCode(dataView.getUint8(offset + i));
|
||||
string += String.fromCharCode(dataView.getUint8(offset + i))
|
||||
}
|
||||
return string;
|
||||
return string
|
||||
}
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
export function getFromPngBuffer(buffer: ArrayBuffer) {
|
||||
// Get the PNG data as a Uint8Array
|
||||
const pngData = new Uint8Array(buffer);
|
||||
const dataView = new DataView(pngData.buffer);
|
||||
const pngData = new Uint8Array(buffer)
|
||||
const dataView = new DataView(pngData.buffer)
|
||||
|
||||
// Check that the PNG signature is present
|
||||
if (dataView.getUint32(0) !== 0x89504e47) {
|
||||
console.error("Not a valid PNG file");
|
||||
return;
|
||||
console.error('Not a valid PNG file')
|
||||
return
|
||||
}
|
||||
|
||||
// Start searching for chunks after the PNG signature
|
||||
let offset = 8;
|
||||
let txt_chunks: Record<string, string> = {};
|
||||
let offset = 8
|
||||
let txt_chunks: Record<string, string> = {}
|
||||
// Loop through the chunks in the PNG file
|
||||
while (offset < pngData.length) {
|
||||
// Get the length of the chunk
|
||||
const length = dataView.getUint32(offset);
|
||||
const length = dataView.getUint32(offset)
|
||||
// Get the chunk type
|
||||
const type = String.fromCharCode(...pngData.slice(offset + 4, offset + 8));
|
||||
if (type === "tEXt" || type == "comf" || type === "iTXt") {
|
||||
const type = String.fromCharCode(...pngData.slice(offset + 4, offset + 8))
|
||||
if (type === 'tEXt' || type == 'comf' || type === 'iTXt') {
|
||||
// Get the keyword
|
||||
let keyword_end = offset + 8;
|
||||
let keyword_end = offset + 8
|
||||
while (pngData[keyword_end] !== 0) {
|
||||
keyword_end++;
|
||||
keyword_end++
|
||||
}
|
||||
const keyword = String.fromCharCode(
|
||||
...pngData.slice(offset + 8, keyword_end)
|
||||
);
|
||||
)
|
||||
// Get the text
|
||||
const contentArraySegment = pngData.slice(
|
||||
keyword_end + 1,
|
||||
offset + 8 + length
|
||||
);
|
||||
const contentJson = new TextDecoder("utf-8").decode(contentArraySegment);
|
||||
txt_chunks[keyword] = contentJson;
|
||||
)
|
||||
const contentJson = new TextDecoder('utf-8').decode(contentArraySegment)
|
||||
txt_chunks[keyword] = contentJson
|
||||
}
|
||||
|
||||
offset += 12 + length;
|
||||
offset += 12 + length
|
||||
}
|
||||
return txt_chunks;
|
||||
return txt_chunks
|
||||
}
|
||||
|
||||
export function getFromPngFile(file: File) {
|
||||
return new Promise<Record<string, string>>((r) => {
|
||||
const reader = new FileReader();
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
r(getFromPngBuffer(event.target.result as ArrayBuffer));
|
||||
};
|
||||
r(getFromPngBuffer(event.target.result as ArrayBuffer))
|
||||
}
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
reader.readAsArrayBuffer(file)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,438 +1,436 @@
|
||||
import { LiteGraph } from "@comfyorg/litegraph";
|
||||
import { api } from "./api";
|
||||
import { getFromPngFile } from "./metadata/png";
|
||||
import { getFromFlacFile } from "./metadata/flac";
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import { api } from './api'
|
||||
import { getFromPngFile } from './metadata/png'
|
||||
import { getFromFlacFile } from './metadata/flac'
|
||||
|
||||
// Original functions left in for backwards compatibility
|
||||
export function getPngMetadata(file: File): Promise<Record<string, string>> {
|
||||
return getFromPngFile(file);
|
||||
return getFromPngFile(file)
|
||||
}
|
||||
|
||||
export function getFlacMetadata(file: File): Promise<Record<string, string>> {
|
||||
return getFromFlacFile(file);
|
||||
return getFromFlacFile(file)
|
||||
}
|
||||
|
||||
function parseExifData(exifData) {
|
||||
// Check for the correct TIFF header (0x4949 for little-endian or 0x4D4D for big-endian)
|
||||
const isLittleEndian = String.fromCharCode(...exifData.slice(0, 2)) === "II";
|
||||
const isLittleEndian = String.fromCharCode(...exifData.slice(0, 2)) === 'II'
|
||||
|
||||
// Function to read 16-bit and 32-bit integers from binary data
|
||||
function readInt(offset, isLittleEndian, length) {
|
||||
let arr = exifData.slice(offset, offset + length);
|
||||
let arr = exifData.slice(offset, offset + length)
|
||||
if (length === 2) {
|
||||
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint16(
|
||||
0,
|
||||
isLittleEndian
|
||||
);
|
||||
)
|
||||
} else if (length === 4) {
|
||||
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint32(
|
||||
0,
|
||||
isLittleEndian
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Read the offset to the first IFD (Image File Directory)
|
||||
const ifdOffset = readInt(4, isLittleEndian, 4);
|
||||
const ifdOffset = readInt(4, isLittleEndian, 4)
|
||||
|
||||
function parseIFD(offset) {
|
||||
const numEntries = readInt(offset, isLittleEndian, 2);
|
||||
const result = {};
|
||||
const numEntries = readInt(offset, isLittleEndian, 2)
|
||||
const result = {}
|
||||
|
||||
for (let i = 0; i < numEntries; i++) {
|
||||
const entryOffset = offset + 2 + i * 12;
|
||||
const tag = readInt(entryOffset, isLittleEndian, 2);
|
||||
const type = readInt(entryOffset + 2, isLittleEndian, 2);
|
||||
const numValues = readInt(entryOffset + 4, isLittleEndian, 4);
|
||||
const valueOffset = readInt(entryOffset + 8, isLittleEndian, 4);
|
||||
const entryOffset = offset + 2 + i * 12
|
||||
const tag = readInt(entryOffset, isLittleEndian, 2)
|
||||
const type = readInt(entryOffset + 2, isLittleEndian, 2)
|
||||
const numValues = readInt(entryOffset + 4, isLittleEndian, 4)
|
||||
const valueOffset = readInt(entryOffset + 8, isLittleEndian, 4)
|
||||
|
||||
// Read the value(s) based on the data type
|
||||
let value;
|
||||
let value
|
||||
if (type === 2) {
|
||||
// ASCII string
|
||||
value = String.fromCharCode(
|
||||
...exifData.slice(valueOffset, valueOffset + numValues - 1)
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
result[tag] = value;
|
||||
result[tag] = value
|
||||
}
|
||||
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
|
||||
// Parse the first IFD
|
||||
const ifdData = parseIFD(ifdOffset);
|
||||
return ifdData;
|
||||
const ifdData = parseIFD(ifdOffset)
|
||||
return ifdData
|
||||
}
|
||||
|
||||
function splitValues(input) {
|
||||
var output = {};
|
||||
var output = {}
|
||||
for (var key in input) {
|
||||
var value = input[key];
|
||||
var splitValues = value.split(":", 2);
|
||||
output[splitValues[0]] = splitValues[1];
|
||||
var value = input[key]
|
||||
var splitValues = value.split(':', 2)
|
||||
output[splitValues[0]] = splitValues[1]
|
||||
}
|
||||
return output;
|
||||
return output
|
||||
}
|
||||
|
||||
export function getWebpMetadata(file) {
|
||||
return new Promise<Record<string, string>>((r) => {
|
||||
const reader = new FileReader();
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
const webp = new Uint8Array(event.target.result as ArrayBuffer);
|
||||
const dataView = new DataView(webp.buffer);
|
||||
const webp = new Uint8Array(event.target.result as ArrayBuffer)
|
||||
const dataView = new DataView(webp.buffer)
|
||||
|
||||
// Check that the WEBP signature is present
|
||||
if (
|
||||
dataView.getUint32(0) !== 0x52494646 ||
|
||||
dataView.getUint32(8) !== 0x57454250
|
||||
) {
|
||||
console.error("Not a valid WEBP file");
|
||||
r({});
|
||||
return;
|
||||
console.error('Not a valid WEBP file')
|
||||
r({})
|
||||
return
|
||||
}
|
||||
|
||||
// Start searching for chunks after the WEBP signature
|
||||
let offset = 12;
|
||||
let txt_chunks = {};
|
||||
let offset = 12
|
||||
let txt_chunks = {}
|
||||
// Loop through the chunks in the WEBP file
|
||||
while (offset < webp.length) {
|
||||
const chunk_length = dataView.getUint32(offset + 4, true);
|
||||
const chunk_length = dataView.getUint32(offset + 4, true)
|
||||
const chunk_type = String.fromCharCode(
|
||||
...webp.slice(offset, offset + 4)
|
||||
);
|
||||
if (chunk_type === "EXIF") {
|
||||
)
|
||||
if (chunk_type === 'EXIF') {
|
||||
if (
|
||||
String.fromCharCode(...webp.slice(offset + 8, offset + 8 + 6)) ==
|
||||
"Exif\0\0"
|
||||
'Exif\0\0'
|
||||
) {
|
||||
offset += 6;
|
||||
offset += 6
|
||||
}
|
||||
let data = parseExifData(
|
||||
webp.slice(offset + 8, offset + 8 + chunk_length)
|
||||
);
|
||||
)
|
||||
for (var key in data) {
|
||||
var value = data[key] as string;
|
||||
let index = value.indexOf(":");
|
||||
txt_chunks[value.slice(0, index)] = value.slice(index + 1);
|
||||
var value = data[key] as string
|
||||
let index = value.indexOf(':')
|
||||
txt_chunks[value.slice(0, index)] = value.slice(index + 1)
|
||||
}
|
||||
break;
|
||||
break
|
||||
}
|
||||
|
||||
offset += 8 + chunk_length;
|
||||
offset += 8 + chunk_length
|
||||
}
|
||||
|
||||
r(txt_chunks);
|
||||
};
|
||||
r(txt_chunks)
|
||||
}
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
reader.readAsArrayBuffer(file)
|
||||
})
|
||||
}
|
||||
|
||||
export function getLatentMetadata(file) {
|
||||
return new Promise((r) => {
|
||||
const reader = new FileReader();
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
const safetensorsData = new Uint8Array(
|
||||
event.target.result as ArrayBuffer
|
||||
);
|
||||
const dataView = new DataView(safetensorsData.buffer);
|
||||
let header_size = dataView.getUint32(0, true);
|
||||
let offset = 8;
|
||||
const safetensorsData = new Uint8Array(event.target.result as ArrayBuffer)
|
||||
const dataView = new DataView(safetensorsData.buffer)
|
||||
let header_size = dataView.getUint32(0, true)
|
||||
let offset = 8
|
||||
let header = JSON.parse(
|
||||
new TextDecoder().decode(
|
||||
safetensorsData.slice(offset, offset + header_size)
|
||||
)
|
||||
);
|
||||
r(header.__metadata__);
|
||||
};
|
||||
)
|
||||
r(header.__metadata__)
|
||||
}
|
||||
|
||||
var slice = file.slice(0, 1024 * 1024 * 4);
|
||||
reader.readAsArrayBuffer(slice);
|
||||
});
|
||||
var slice = file.slice(0, 1024 * 1024 * 4)
|
||||
reader.readAsArrayBuffer(slice)
|
||||
})
|
||||
}
|
||||
|
||||
export async function importA1111(graph, parameters) {
|
||||
const p = parameters.lastIndexOf("\nSteps:");
|
||||
const p = parameters.lastIndexOf('\nSteps:')
|
||||
if (p > -1) {
|
||||
const embeddings = await api.getEmbeddings();
|
||||
const embeddings = await api.getEmbeddings()
|
||||
const opts = parameters
|
||||
.substr(p)
|
||||
.split("\n")[1]
|
||||
.split('\n')[1]
|
||||
.match(
|
||||
new RegExp('\\s*([^:]+:\\s*([^"\\{].*?|".*?"|\\{.*?\\}))\\s*(,|$)', "g")
|
||||
new RegExp('\\s*([^:]+:\\s*([^"\\{].*?|".*?"|\\{.*?\\}))\\s*(,|$)', 'g')
|
||||
)
|
||||
.reduce((p, n) => {
|
||||
const s = n.split(":");
|
||||
if (s[1].endsWith(",")) {
|
||||
s[1] = s[1].substr(0, s[1].length - 1);
|
||||
const s = n.split(':')
|
||||
if (s[1].endsWith(',')) {
|
||||
s[1] = s[1].substr(0, s[1].length - 1)
|
||||
}
|
||||
p[s[0].trim().toLowerCase()] = s[1].trim();
|
||||
return p;
|
||||
}, {});
|
||||
const p2 = parameters.lastIndexOf("\nNegative prompt:", p);
|
||||
p[s[0].trim().toLowerCase()] = s[1].trim()
|
||||
return p
|
||||
}, {})
|
||||
const p2 = parameters.lastIndexOf('\nNegative prompt:', p)
|
||||
if (p2 > -1) {
|
||||
let positive = parameters.substr(0, p2).trim();
|
||||
let negative = parameters.substring(p2 + 18, p).trim();
|
||||
let positive = parameters.substr(0, p2).trim()
|
||||
let negative = parameters.substring(p2 + 18, p).trim()
|
||||
|
||||
const ckptNode = LiteGraph.createNode("CheckpointLoaderSimple");
|
||||
const clipSkipNode = LiteGraph.createNode("CLIPSetLastLayer");
|
||||
const positiveNode = LiteGraph.createNode("CLIPTextEncode");
|
||||
const negativeNode = LiteGraph.createNode("CLIPTextEncode");
|
||||
const samplerNode = LiteGraph.createNode("KSampler");
|
||||
const imageNode = LiteGraph.createNode("EmptyLatentImage");
|
||||
const vaeNode = LiteGraph.createNode("VAEDecode");
|
||||
const vaeLoaderNode = LiteGraph.createNode("VAELoader");
|
||||
const saveNode = LiteGraph.createNode("SaveImage");
|
||||
let hrSamplerNode = null;
|
||||
let hrSteps = null;
|
||||
const ckptNode = LiteGraph.createNode('CheckpointLoaderSimple')
|
||||
const clipSkipNode = LiteGraph.createNode('CLIPSetLastLayer')
|
||||
const positiveNode = LiteGraph.createNode('CLIPTextEncode')
|
||||
const negativeNode = LiteGraph.createNode('CLIPTextEncode')
|
||||
const samplerNode = LiteGraph.createNode('KSampler')
|
||||
const imageNode = LiteGraph.createNode('EmptyLatentImage')
|
||||
const vaeNode = LiteGraph.createNode('VAEDecode')
|
||||
const vaeLoaderNode = LiteGraph.createNode('VAELoader')
|
||||
const saveNode = LiteGraph.createNode('SaveImage')
|
||||
let hrSamplerNode = null
|
||||
let hrSteps = null
|
||||
|
||||
const ceil64 = (v) => Math.ceil(v / 64) * 64;
|
||||
const ceil64 = (v) => Math.ceil(v / 64) * 64
|
||||
|
||||
const getWidget = (node, name) => {
|
||||
return node.widgets.find((w) => w.name === name);
|
||||
};
|
||||
return node.widgets.find((w) => w.name === name)
|
||||
}
|
||||
|
||||
const setWidgetValue = (node, name, value, isOptionPrefix?) => {
|
||||
const w = getWidget(node, name);
|
||||
const w = getWidget(node, name)
|
||||
if (isOptionPrefix) {
|
||||
const o = w.options.values.find((w) => w.startsWith(value));
|
||||
const o = w.options.values.find((w) => w.startsWith(value))
|
||||
if (o) {
|
||||
w.value = o;
|
||||
w.value = o
|
||||
} else {
|
||||
console.warn(`Unknown value '${value}' for widget '${name}'`, node);
|
||||
w.value = value;
|
||||
console.warn(`Unknown value '${value}' for widget '${name}'`, node)
|
||||
w.value = value
|
||||
}
|
||||
} else {
|
||||
w.value = value;
|
||||
w.value = value
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createLoraNodes = (clipNode, text, prevClip, prevModel) => {
|
||||
const loras = [];
|
||||
const loras = []
|
||||
text = text.replace(/<lora:([^:]+:[^>]+)>/g, function (m, c) {
|
||||
const s = c.split(":");
|
||||
const weight = parseFloat(s[1]);
|
||||
const s = c.split(':')
|
||||
const weight = parseFloat(s[1])
|
||||
if (isNaN(weight)) {
|
||||
console.warn("Invalid LORA", m);
|
||||
console.warn('Invalid LORA', m)
|
||||
} else {
|
||||
loras.push({ name: s[0], weight });
|
||||
loras.push({ name: s[0], weight })
|
||||
}
|
||||
return "";
|
||||
});
|
||||
return ''
|
||||
})
|
||||
|
||||
for (const l of loras) {
|
||||
const loraNode = LiteGraph.createNode("LoraLoader");
|
||||
graph.add(loraNode);
|
||||
setWidgetValue(loraNode, "lora_name", l.name, true);
|
||||
setWidgetValue(loraNode, "strength_model", l.weight);
|
||||
setWidgetValue(loraNode, "strength_clip", l.weight);
|
||||
prevModel.node.connect(prevModel.index, loraNode, 0);
|
||||
prevClip.node.connect(prevClip.index, loraNode, 1);
|
||||
prevModel = { node: loraNode, index: 0 };
|
||||
prevClip = { node: loraNode, index: 1 };
|
||||
const loraNode = LiteGraph.createNode('LoraLoader')
|
||||
graph.add(loraNode)
|
||||
setWidgetValue(loraNode, 'lora_name', l.name, true)
|
||||
setWidgetValue(loraNode, 'strength_model', l.weight)
|
||||
setWidgetValue(loraNode, 'strength_clip', l.weight)
|
||||
prevModel.node.connect(prevModel.index, loraNode, 0)
|
||||
prevClip.node.connect(prevClip.index, loraNode, 1)
|
||||
prevModel = { node: loraNode, index: 0 }
|
||||
prevClip = { node: loraNode, index: 1 }
|
||||
}
|
||||
|
||||
prevClip.node.connect(1, clipNode, 0);
|
||||
prevModel.node.connect(0, samplerNode, 0);
|
||||
prevClip.node.connect(1, clipNode, 0)
|
||||
prevModel.node.connect(0, samplerNode, 0)
|
||||
if (hrSamplerNode) {
|
||||
prevModel.node.connect(0, hrSamplerNode, 0);
|
||||
prevModel.node.connect(0, hrSamplerNode, 0)
|
||||
}
|
||||
|
||||
return { text, prevModel, prevClip };
|
||||
};
|
||||
return { text, prevModel, prevClip }
|
||||
}
|
||||
|
||||
const replaceEmbeddings = (text) => {
|
||||
if (!embeddings.length) return text;
|
||||
if (!embeddings.length) return text
|
||||
return text.replaceAll(
|
||||
new RegExp(
|
||||
"\\b(" +
|
||||
'\\b(' +
|
||||
embeddings
|
||||
.map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
||||
.join("\\b|\\b") +
|
||||
")\\b",
|
||||
"ig"
|
||||
.map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
||||
.join('\\b|\\b') +
|
||||
')\\b',
|
||||
'ig'
|
||||
),
|
||||
"embedding:$1"
|
||||
);
|
||||
};
|
||||
'embedding:$1'
|
||||
)
|
||||
}
|
||||
|
||||
const popOpt = (name) => {
|
||||
const v = opts[name];
|
||||
delete opts[name];
|
||||
return v;
|
||||
};
|
||||
const v = opts[name]
|
||||
delete opts[name]
|
||||
return v
|
||||
}
|
||||
|
||||
graph.clear();
|
||||
graph.add(ckptNode);
|
||||
graph.add(clipSkipNode);
|
||||
graph.add(positiveNode);
|
||||
graph.add(negativeNode);
|
||||
graph.add(samplerNode);
|
||||
graph.add(imageNode);
|
||||
graph.add(vaeNode);
|
||||
graph.add(vaeLoaderNode);
|
||||
graph.add(saveNode);
|
||||
graph.clear()
|
||||
graph.add(ckptNode)
|
||||
graph.add(clipSkipNode)
|
||||
graph.add(positiveNode)
|
||||
graph.add(negativeNode)
|
||||
graph.add(samplerNode)
|
||||
graph.add(imageNode)
|
||||
graph.add(vaeNode)
|
||||
graph.add(vaeLoaderNode)
|
||||
graph.add(saveNode)
|
||||
|
||||
ckptNode.connect(1, clipSkipNode, 0);
|
||||
clipSkipNode.connect(0, positiveNode, 0);
|
||||
clipSkipNode.connect(0, negativeNode, 0);
|
||||
ckptNode.connect(0, samplerNode, 0);
|
||||
positiveNode.connect(0, samplerNode, 1);
|
||||
negativeNode.connect(0, samplerNode, 2);
|
||||
imageNode.connect(0, samplerNode, 3);
|
||||
vaeNode.connect(0, saveNode, 0);
|
||||
samplerNode.connect(0, vaeNode, 0);
|
||||
vaeLoaderNode.connect(0, vaeNode, 1);
|
||||
ckptNode.connect(1, clipSkipNode, 0)
|
||||
clipSkipNode.connect(0, positiveNode, 0)
|
||||
clipSkipNode.connect(0, negativeNode, 0)
|
||||
ckptNode.connect(0, samplerNode, 0)
|
||||
positiveNode.connect(0, samplerNode, 1)
|
||||
negativeNode.connect(0, samplerNode, 2)
|
||||
imageNode.connect(0, samplerNode, 3)
|
||||
vaeNode.connect(0, saveNode, 0)
|
||||
samplerNode.connect(0, vaeNode, 0)
|
||||
vaeLoaderNode.connect(0, vaeNode, 1)
|
||||
|
||||
const handlers = {
|
||||
model(v) {
|
||||
setWidgetValue(ckptNode, "ckpt_name", v, true);
|
||||
setWidgetValue(ckptNode, 'ckpt_name', v, true)
|
||||
},
|
||||
vae(v) {
|
||||
setWidgetValue(vaeLoaderNode, "vae_name", v, true);
|
||||
setWidgetValue(vaeLoaderNode, 'vae_name', v, true)
|
||||
},
|
||||
"cfg scale"(v) {
|
||||
setWidgetValue(samplerNode, "cfg", +v);
|
||||
'cfg scale'(v) {
|
||||
setWidgetValue(samplerNode, 'cfg', +v)
|
||||
},
|
||||
"clip skip"(v) {
|
||||
setWidgetValue(clipSkipNode, "stop_at_clip_layer", -v);
|
||||
'clip skip'(v) {
|
||||
setWidgetValue(clipSkipNode, 'stop_at_clip_layer', -v)
|
||||
},
|
||||
sampler(v) {
|
||||
let name = v.toLowerCase().replace("++", "pp").replaceAll(" ", "_");
|
||||
if (name.includes("karras")) {
|
||||
name = name.replace("karras", "").replace(/_+$/, "");
|
||||
setWidgetValue(samplerNode, "scheduler", "karras");
|
||||
let name = v.toLowerCase().replace('++', 'pp').replaceAll(' ', '_')
|
||||
if (name.includes('karras')) {
|
||||
name = name.replace('karras', '').replace(/_+$/, '')
|
||||
setWidgetValue(samplerNode, 'scheduler', 'karras')
|
||||
} else {
|
||||
setWidgetValue(samplerNode, "scheduler", "normal");
|
||||
setWidgetValue(samplerNode, 'scheduler', 'normal')
|
||||
}
|
||||
const w = getWidget(samplerNode, "sampler_name");
|
||||
const w = getWidget(samplerNode, 'sampler_name')
|
||||
const o = w.options.values.find(
|
||||
(w) => w === name || w === "sample_" + name
|
||||
);
|
||||
(w) => w === name || w === 'sample_' + name
|
||||
)
|
||||
if (o) {
|
||||
setWidgetValue(samplerNode, "sampler_name", o);
|
||||
setWidgetValue(samplerNode, 'sampler_name', o)
|
||||
}
|
||||
},
|
||||
size(v) {
|
||||
const wxh = v.split("x");
|
||||
const w = ceil64(+wxh[0]);
|
||||
const h = ceil64(+wxh[1]);
|
||||
const hrUp = popOpt("hires upscale");
|
||||
const hrSz = popOpt("hires resize");
|
||||
hrSteps = popOpt("hires steps");
|
||||
let hrMethod = popOpt("hires upscaler");
|
||||
const wxh = v.split('x')
|
||||
const w = ceil64(+wxh[0])
|
||||
const h = ceil64(+wxh[1])
|
||||
const hrUp = popOpt('hires upscale')
|
||||
const hrSz = popOpt('hires resize')
|
||||
hrSteps = popOpt('hires steps')
|
||||
let hrMethod = popOpt('hires upscaler')
|
||||
|
||||
setWidgetValue(imageNode, "width", w);
|
||||
setWidgetValue(imageNode, "height", h);
|
||||
setWidgetValue(imageNode, 'width', w)
|
||||
setWidgetValue(imageNode, 'height', h)
|
||||
|
||||
if (hrUp || hrSz) {
|
||||
let uw, uh;
|
||||
let uw, uh
|
||||
if (hrUp) {
|
||||
uw = w * hrUp;
|
||||
uh = h * hrUp;
|
||||
uw = w * hrUp
|
||||
uh = h * hrUp
|
||||
} else {
|
||||
const s = hrSz.split("x");
|
||||
uw = +s[0];
|
||||
uh = +s[1];
|
||||
const s = hrSz.split('x')
|
||||
uw = +s[0]
|
||||
uh = +s[1]
|
||||
}
|
||||
|
||||
let upscaleNode;
|
||||
let latentNode;
|
||||
let upscaleNode
|
||||
let latentNode
|
||||
|
||||
if (hrMethod.startsWith("Latent")) {
|
||||
latentNode = upscaleNode = LiteGraph.createNode("LatentUpscale");
|
||||
graph.add(upscaleNode);
|
||||
samplerNode.connect(0, upscaleNode, 0);
|
||||
if (hrMethod.startsWith('Latent')) {
|
||||
latentNode = upscaleNode = LiteGraph.createNode('LatentUpscale')
|
||||
graph.add(upscaleNode)
|
||||
samplerNode.connect(0, upscaleNode, 0)
|
||||
|
||||
switch (hrMethod) {
|
||||
case "Latent (nearest-exact)":
|
||||
hrMethod = "nearest-exact";
|
||||
break;
|
||||
case 'Latent (nearest-exact)':
|
||||
hrMethod = 'nearest-exact'
|
||||
break
|
||||
}
|
||||
setWidgetValue(upscaleNode, "upscale_method", hrMethod, true);
|
||||
setWidgetValue(upscaleNode, 'upscale_method', hrMethod, true)
|
||||
} else {
|
||||
const decode = LiteGraph.createNode("VAEDecodeTiled");
|
||||
graph.add(decode);
|
||||
samplerNode.connect(0, decode, 0);
|
||||
vaeLoaderNode.connect(0, decode, 1);
|
||||
const decode = LiteGraph.createNode('VAEDecodeTiled')
|
||||
graph.add(decode)
|
||||
samplerNode.connect(0, decode, 0)
|
||||
vaeLoaderNode.connect(0, decode, 1)
|
||||
|
||||
const upscaleLoaderNode =
|
||||
LiteGraph.createNode("UpscaleModelLoader");
|
||||
graph.add(upscaleLoaderNode);
|
||||
setWidgetValue(upscaleLoaderNode, "model_name", hrMethod, true);
|
||||
LiteGraph.createNode('UpscaleModelLoader')
|
||||
graph.add(upscaleLoaderNode)
|
||||
setWidgetValue(upscaleLoaderNode, 'model_name', hrMethod, true)
|
||||
|
||||
const modelUpscaleNode = LiteGraph.createNode(
|
||||
"ImageUpscaleWithModel"
|
||||
);
|
||||
graph.add(modelUpscaleNode);
|
||||
decode.connect(0, modelUpscaleNode, 1);
|
||||
upscaleLoaderNode.connect(0, modelUpscaleNode, 0);
|
||||
'ImageUpscaleWithModel'
|
||||
)
|
||||
graph.add(modelUpscaleNode)
|
||||
decode.connect(0, modelUpscaleNode, 1)
|
||||
upscaleLoaderNode.connect(0, modelUpscaleNode, 0)
|
||||
|
||||
upscaleNode = LiteGraph.createNode("ImageScale");
|
||||
graph.add(upscaleNode);
|
||||
modelUpscaleNode.connect(0, upscaleNode, 0);
|
||||
upscaleNode = LiteGraph.createNode('ImageScale')
|
||||
graph.add(upscaleNode)
|
||||
modelUpscaleNode.connect(0, upscaleNode, 0)
|
||||
|
||||
const vaeEncodeNode = (latentNode =
|
||||
LiteGraph.createNode("VAEEncodeTiled"));
|
||||
graph.add(vaeEncodeNode);
|
||||
upscaleNode.connect(0, vaeEncodeNode, 0);
|
||||
vaeLoaderNode.connect(0, vaeEncodeNode, 1);
|
||||
LiteGraph.createNode('VAEEncodeTiled'))
|
||||
graph.add(vaeEncodeNode)
|
||||
upscaleNode.connect(0, vaeEncodeNode, 0)
|
||||
vaeLoaderNode.connect(0, vaeEncodeNode, 1)
|
||||
}
|
||||
|
||||
setWidgetValue(upscaleNode, "width", ceil64(uw));
|
||||
setWidgetValue(upscaleNode, "height", ceil64(uh));
|
||||
setWidgetValue(upscaleNode, 'width', ceil64(uw))
|
||||
setWidgetValue(upscaleNode, 'height', ceil64(uh))
|
||||
|
||||
hrSamplerNode = LiteGraph.createNode("KSampler");
|
||||
graph.add(hrSamplerNode);
|
||||
ckptNode.connect(0, hrSamplerNode, 0);
|
||||
positiveNode.connect(0, hrSamplerNode, 1);
|
||||
negativeNode.connect(0, hrSamplerNode, 2);
|
||||
latentNode.connect(0, hrSamplerNode, 3);
|
||||
hrSamplerNode.connect(0, vaeNode, 0);
|
||||
hrSamplerNode = LiteGraph.createNode('KSampler')
|
||||
graph.add(hrSamplerNode)
|
||||
ckptNode.connect(0, hrSamplerNode, 0)
|
||||
positiveNode.connect(0, hrSamplerNode, 1)
|
||||
negativeNode.connect(0, hrSamplerNode, 2)
|
||||
latentNode.connect(0, hrSamplerNode, 3)
|
||||
hrSamplerNode.connect(0, vaeNode, 0)
|
||||
}
|
||||
},
|
||||
steps(v) {
|
||||
setWidgetValue(samplerNode, "steps", +v);
|
||||
setWidgetValue(samplerNode, 'steps', +v)
|
||||
},
|
||||
seed(v) {
|
||||
setWidgetValue(samplerNode, "seed", +v);
|
||||
},
|
||||
};
|
||||
setWidgetValue(samplerNode, 'seed', +v)
|
||||
}
|
||||
}
|
||||
|
||||
for (const opt in opts) {
|
||||
if (opt in handlers) {
|
||||
handlers[opt](popOpt(opt));
|
||||
handlers[opt](popOpt(opt))
|
||||
}
|
||||
}
|
||||
|
||||
if (hrSamplerNode) {
|
||||
setWidgetValue(
|
||||
hrSamplerNode,
|
||||
"steps",
|
||||
hrSteps ? +hrSteps : getWidget(samplerNode, "steps").value
|
||||
);
|
||||
'steps',
|
||||
hrSteps ? +hrSteps : getWidget(samplerNode, 'steps').value
|
||||
)
|
||||
setWidgetValue(
|
||||
hrSamplerNode,
|
||||
"cfg",
|
||||
getWidget(samplerNode, "cfg").value
|
||||
);
|
||||
'cfg',
|
||||
getWidget(samplerNode, 'cfg').value
|
||||
)
|
||||
setWidgetValue(
|
||||
hrSamplerNode,
|
||||
"scheduler",
|
||||
getWidget(samplerNode, "scheduler").value
|
||||
);
|
||||
'scheduler',
|
||||
getWidget(samplerNode, 'scheduler').value
|
||||
)
|
||||
setWidgetValue(
|
||||
hrSamplerNode,
|
||||
"sampler_name",
|
||||
getWidget(samplerNode, "sampler_name").value
|
||||
);
|
||||
'sampler_name',
|
||||
getWidget(samplerNode, 'sampler_name').value
|
||||
)
|
||||
setWidgetValue(
|
||||
hrSamplerNode,
|
||||
"denoise",
|
||||
+(popOpt("denoising strength") || "1")
|
||||
);
|
||||
'denoise',
|
||||
+(popOpt('denoising strength') || '1')
|
||||
)
|
||||
}
|
||||
|
||||
let n = createLoraNodes(
|
||||
@@ -440,29 +438,29 @@ export async function importA1111(graph, parameters) {
|
||||
positive,
|
||||
{ node: clipSkipNode, index: 0 },
|
||||
{ node: ckptNode, index: 0 }
|
||||
);
|
||||
positive = n.text;
|
||||
n = createLoraNodes(negativeNode, negative, n.prevClip, n.prevModel);
|
||||
negative = n.text;
|
||||
)
|
||||
positive = n.text
|
||||
n = createLoraNodes(negativeNode, negative, n.prevClip, n.prevModel)
|
||||
negative = n.text
|
||||
|
||||
setWidgetValue(positiveNode, "text", replaceEmbeddings(positive));
|
||||
setWidgetValue(negativeNode, "text", replaceEmbeddings(negative));
|
||||
setWidgetValue(positiveNode, 'text', replaceEmbeddings(positive))
|
||||
setWidgetValue(negativeNode, 'text', replaceEmbeddings(negative))
|
||||
|
||||
graph.arrange();
|
||||
graph.arrange()
|
||||
|
||||
for (const opt of [
|
||||
"model hash",
|
||||
"ensd",
|
||||
"version",
|
||||
"vae hash",
|
||||
"ti hashes",
|
||||
"lora hashes",
|
||||
"hashes",
|
||||
'model hash',
|
||||
'ensd',
|
||||
'version',
|
||||
'vae hash',
|
||||
'ti hashes',
|
||||
'lora hashes',
|
||||
'hashes'
|
||||
]) {
|
||||
delete opts[opt];
|
||||
delete opts[opt]
|
||||
}
|
||||
|
||||
console.warn("Unhandled parameters:", opts);
|
||||
console.warn('Unhandled parameters:', opts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,64 +1,64 @@
|
||||
import { ComfyDialog } from "../dialog";
|
||||
import { $el } from "../../ui";
|
||||
import { ComfyDialog } from '../dialog'
|
||||
import { $el } from '../../ui'
|
||||
|
||||
export class ComfyAsyncDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
#resolve: (value: any) => void;
|
||||
#resolve: (value: any) => void
|
||||
|
||||
constructor(actions?: Array<string | { value?: any; text: string }>) {
|
||||
super(
|
||||
"dialog.comfy-dialog.comfyui-dialog",
|
||||
'dialog.comfy-dialog.comfyui-dialog',
|
||||
actions?.map((opt) => {
|
||||
if (typeof opt === "string") {
|
||||
opt = { text: opt };
|
||||
if (typeof opt === 'string') {
|
||||
opt = { text: opt }
|
||||
}
|
||||
return $el("button.comfyui-button", {
|
||||
type: "button",
|
||||
return $el('button.comfyui-button', {
|
||||
type: 'button',
|
||||
textContent: opt.text,
|
||||
onclick: () => this.close(opt.value ?? opt.text),
|
||||
});
|
||||
onclick: () => this.close(opt.value ?? opt.text)
|
||||
})
|
||||
);
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
show(html: string | HTMLElement | HTMLElement[]) {
|
||||
this.element.addEventListener("close", () => {
|
||||
this.close();
|
||||
});
|
||||
this.element.addEventListener('close', () => {
|
||||
this.close()
|
||||
})
|
||||
|
||||
super.show(html);
|
||||
super.show(html)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.#resolve = resolve;
|
||||
});
|
||||
this.#resolve = resolve
|
||||
})
|
||||
}
|
||||
|
||||
showModal(html: string | HTMLElement | HTMLElement[]) {
|
||||
this.element.addEventListener("close", () => {
|
||||
this.close();
|
||||
});
|
||||
this.element.addEventListener('close', () => {
|
||||
this.close()
|
||||
})
|
||||
|
||||
super.show(html);
|
||||
this.element.showModal();
|
||||
super.show(html)
|
||||
this.element.showModal()
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.#resolve = resolve;
|
||||
});
|
||||
this.#resolve = resolve
|
||||
})
|
||||
}
|
||||
|
||||
close(result = null) {
|
||||
this.#resolve(result);
|
||||
this.element.close();
|
||||
super.close();
|
||||
this.#resolve(result)
|
||||
this.element.close()
|
||||
super.close()
|
||||
}
|
||||
|
||||
static async prompt({ title = null, message, actions }) {
|
||||
const dialog = new ComfyAsyncDialog(actions);
|
||||
const content = [$el("span", message)];
|
||||
const dialog = new ComfyAsyncDialog(actions)
|
||||
const content = [$el('span', message)]
|
||||
if (title) {
|
||||
content.unshift($el("h3", title));
|
||||
content.unshift($el('h3', title))
|
||||
}
|
||||
const res = await dialog.showModal(content);
|
||||
dialog.element.remove();
|
||||
return res;
|
||||
const res = await dialog.showModal(content)
|
||||
dialog.element.remove()
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
import { $el } from "../../ui";
|
||||
import { applyClasses, ClassList, toggleElement } from "../utils";
|
||||
import { prop } from "../../utils";
|
||||
import type { ComfyPopup } from "./popup";
|
||||
import type { ComfyComponent } from ".";
|
||||
import type { ComfyApp } from "@/scripts/app";
|
||||
import { $el } from '../../ui'
|
||||
import { applyClasses, ClassList, toggleElement } from '../utils'
|
||||
import { prop } from '../../utils'
|
||||
import type { ComfyPopup } from './popup'
|
||||
import type { ComfyComponent } from '.'
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
|
||||
type ComfyButtonProps = {
|
||||
icon?: string;
|
||||
overIcon?: string;
|
||||
iconSize?: number;
|
||||
content?: string | HTMLElement;
|
||||
tooltip?: string;
|
||||
enabled?: boolean;
|
||||
action?: (e: Event, btn: ComfyButton) => void;
|
||||
classList?: ClassList;
|
||||
visibilitySetting?: { id: string; showValue: any };
|
||||
app?: ComfyApp;
|
||||
};
|
||||
icon?: string
|
||||
overIcon?: string
|
||||
iconSize?: number
|
||||
content?: string | HTMLElement
|
||||
tooltip?: string
|
||||
enabled?: boolean
|
||||
action?: (e: Event, btn: ComfyButton) => void
|
||||
classList?: ClassList
|
||||
visibilitySetting?: { id: string; showValue: any }
|
||||
app?: ComfyApp
|
||||
}
|
||||
|
||||
export class ComfyButton implements ComfyComponent<HTMLElement> {
|
||||
#over = 0;
|
||||
#popupOpen = false;
|
||||
isOver = false;
|
||||
iconElement = $el("i.mdi");
|
||||
contentElement = $el("span");
|
||||
popup: ComfyPopup;
|
||||
element: HTMLElement;
|
||||
overIcon: string;
|
||||
iconSize: number;
|
||||
content: string | HTMLElement;
|
||||
icon: string;
|
||||
tooltip: string;
|
||||
classList: ClassList;
|
||||
hidden: boolean;
|
||||
enabled: boolean;
|
||||
action: (e: Event, btn: ComfyButton) => void;
|
||||
#over = 0
|
||||
#popupOpen = false
|
||||
isOver = false
|
||||
iconElement = $el('i.mdi')
|
||||
contentElement = $el('span')
|
||||
popup: ComfyPopup
|
||||
element: HTMLElement
|
||||
overIcon: string
|
||||
iconSize: number
|
||||
content: string | HTMLElement
|
||||
icon: string
|
||||
tooltip: string
|
||||
classList: ClassList
|
||||
hidden: boolean
|
||||
enabled: boolean
|
||||
action: (e: Event, btn: ComfyButton) => void
|
||||
|
||||
constructor({
|
||||
icon,
|
||||
@@ -43,134 +43,134 @@ export class ComfyButton implements ComfyComponent<HTMLElement> {
|
||||
content,
|
||||
tooltip,
|
||||
action,
|
||||
classList = "comfyui-button",
|
||||
classList = 'comfyui-button',
|
||||
visibilitySetting,
|
||||
app,
|
||||
enabled = true,
|
||||
enabled = true
|
||||
}: ComfyButtonProps) {
|
||||
this.element = $el(
|
||||
"button",
|
||||
'button',
|
||||
{
|
||||
onmouseenter: () => {
|
||||
this.isOver = true;
|
||||
this.isOver = true
|
||||
if (this.overIcon) {
|
||||
this.updateIcon();
|
||||
this.updateIcon()
|
||||
}
|
||||
},
|
||||
onmouseleave: () => {
|
||||
this.isOver = false;
|
||||
this.isOver = false
|
||||
if (this.overIcon) {
|
||||
this.updateIcon();
|
||||
this.updateIcon()
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
[this.iconElement, this.contentElement]
|
||||
);
|
||||
)
|
||||
|
||||
this.icon = prop(
|
||||
this,
|
||||
"icon",
|
||||
'icon',
|
||||
icon,
|
||||
toggleElement(this.iconElement, { onShow: this.updateIcon })
|
||||
);
|
||||
this.overIcon = prop(this, "overIcon", overIcon, () => {
|
||||
)
|
||||
this.overIcon = prop(this, 'overIcon', overIcon, () => {
|
||||
if (this.isOver) {
|
||||
this.updateIcon();
|
||||
this.updateIcon()
|
||||
}
|
||||
});
|
||||
this.iconSize = prop(this, "iconSize", iconSize, this.updateIcon);
|
||||
})
|
||||
this.iconSize = prop(this, 'iconSize', iconSize, this.updateIcon)
|
||||
this.content = prop(
|
||||
this,
|
||||
"content",
|
||||
'content',
|
||||
content,
|
||||
toggleElement(this.contentElement, {
|
||||
onShow: (el, v) => {
|
||||
if (typeof v === "string") {
|
||||
el.textContent = v;
|
||||
if (typeof v === 'string') {
|
||||
el.textContent = v
|
||||
} else {
|
||||
el.replaceChildren(v);
|
||||
el.replaceChildren(v)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
this.tooltip = prop(this, "tooltip", tooltip, (v) => {
|
||||
this.tooltip = prop(this, 'tooltip', tooltip, (v) => {
|
||||
if (v) {
|
||||
this.element.title = v;
|
||||
this.element.title = v
|
||||
} else {
|
||||
this.element.removeAttribute("title");
|
||||
this.element.removeAttribute('title')
|
||||
}
|
||||
});
|
||||
this.classList = prop(this, "classList", classList, this.updateClasses);
|
||||
this.hidden = prop(this, "hidden", false, this.updateClasses);
|
||||
this.enabled = prop(this, "enabled", enabled, () => {
|
||||
this.updateClasses();
|
||||
(this.element as HTMLButtonElement).disabled = !this.enabled;
|
||||
});
|
||||
this.action = prop(this, "action", action);
|
||||
this.element.addEventListener("click", (e) => {
|
||||
})
|
||||
this.classList = prop(this, 'classList', classList, this.updateClasses)
|
||||
this.hidden = prop(this, 'hidden', false, this.updateClasses)
|
||||
this.enabled = prop(this, 'enabled', enabled, () => {
|
||||
this.updateClasses()
|
||||
;(this.element as HTMLButtonElement).disabled = !this.enabled
|
||||
})
|
||||
this.action = prop(this, 'action', action)
|
||||
this.element.addEventListener('click', (e) => {
|
||||
if (this.popup) {
|
||||
// we are either a touch device or triggered by click not hover
|
||||
if (!this.#over) {
|
||||
this.popup.toggle();
|
||||
this.popup.toggle()
|
||||
}
|
||||
}
|
||||
this.action?.(e, this);
|
||||
});
|
||||
this.action?.(e, this)
|
||||
})
|
||||
|
||||
if (visibilitySetting?.id) {
|
||||
const settingUpdated = () => {
|
||||
this.hidden =
|
||||
app.ui.settings.getSettingValue(visibilitySetting.id) !==
|
||||
visibilitySetting.showValue;
|
||||
};
|
||||
visibilitySetting.showValue
|
||||
}
|
||||
app.ui.settings.addEventListener(
|
||||
visibilitySetting.id + ".change",
|
||||
visibilitySetting.id + '.change',
|
||||
settingUpdated
|
||||
);
|
||||
settingUpdated();
|
||||
)
|
||||
settingUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
updateIcon = () =>
|
||||
(this.iconElement.className = `mdi mdi-${(this.isOver && this.overIcon) || this.icon}${this.iconSize ? " mdi-" + this.iconSize + "px" : ""}`);
|
||||
(this.iconElement.className = `mdi mdi-${(this.isOver && this.overIcon) || this.icon}${this.iconSize ? ' mdi-' + this.iconSize + 'px' : ''}`)
|
||||
updateClasses = () => {
|
||||
const internalClasses = [];
|
||||
const internalClasses = []
|
||||
if (this.hidden) {
|
||||
internalClasses.push("hidden");
|
||||
internalClasses.push('hidden')
|
||||
}
|
||||
if (!this.enabled) {
|
||||
internalClasses.push("disabled");
|
||||
internalClasses.push('disabled')
|
||||
}
|
||||
if (this.popup) {
|
||||
if (this.#popupOpen) {
|
||||
internalClasses.push("popup-open");
|
||||
internalClasses.push('popup-open')
|
||||
} else {
|
||||
internalClasses.push("popup-closed");
|
||||
internalClasses.push('popup-closed')
|
||||
}
|
||||
}
|
||||
applyClasses(this.element, this.classList, ...internalClasses);
|
||||
};
|
||||
applyClasses(this.element, this.classList, ...internalClasses)
|
||||
}
|
||||
|
||||
withPopup(popup: ComfyPopup, mode: "click" | "hover" = "click") {
|
||||
this.popup = popup;
|
||||
withPopup(popup: ComfyPopup, mode: 'click' | 'hover' = 'click') {
|
||||
this.popup = popup
|
||||
|
||||
if (mode === "hover") {
|
||||
if (mode === 'hover') {
|
||||
for (const el of [this.element, this.popup.element]) {
|
||||
el.addEventListener("mouseenter", () => {
|
||||
this.popup.open = !!++this.#over;
|
||||
});
|
||||
el.addEventListener("mouseleave", () => {
|
||||
this.popup.open = !!--this.#over;
|
||||
});
|
||||
el.addEventListener('mouseenter', () => {
|
||||
this.popup.open = !!++this.#over
|
||||
})
|
||||
el.addEventListener('mouseleave', () => {
|
||||
this.popup.open = !!--this.#over
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
popup.addEventListener("change", () => {
|
||||
this.#popupOpen = popup.open;
|
||||
this.updateClasses();
|
||||
});
|
||||
popup.addEventListener('change', () => {
|
||||
this.#popupOpen = popup.open
|
||||
this.updateClasses()
|
||||
})
|
||||
|
||||
return this;
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
import { $el } from "../../ui";
|
||||
import { ComfyButton } from "./button";
|
||||
import { prop } from "../../utils";
|
||||
import { $el } from '../../ui'
|
||||
import { ComfyButton } from './button'
|
||||
import { prop } from '../../utils'
|
||||
|
||||
export class ComfyButtonGroup {
|
||||
element = $el("div.comfyui-button-group");
|
||||
buttons: (HTMLElement | ComfyButton)[];
|
||||
element = $el('div.comfyui-button-group')
|
||||
buttons: (HTMLElement | ComfyButton)[]
|
||||
|
||||
constructor(...buttons: (HTMLElement | ComfyButton)[]) {
|
||||
this.buttons = prop(this, "buttons", buttons, () => this.update());
|
||||
this.buttons = prop(this, 'buttons', buttons, () => this.update())
|
||||
}
|
||||
|
||||
insert(button: ComfyButton, index: number) {
|
||||
this.buttons.splice(index, 0, button);
|
||||
this.update();
|
||||
this.buttons.splice(index, 0, button)
|
||||
this.update()
|
||||
}
|
||||
|
||||
append(button: ComfyButton) {
|
||||
this.buttons.push(button);
|
||||
this.update();
|
||||
this.buttons.push(button)
|
||||
this.update()
|
||||
}
|
||||
|
||||
remove(indexOrButton: ComfyButton | number) {
|
||||
if (typeof indexOrButton !== "number") {
|
||||
indexOrButton = this.buttons.indexOf(indexOrButton);
|
||||
if (typeof indexOrButton !== 'number') {
|
||||
indexOrButton = this.buttons.indexOf(indexOrButton)
|
||||
}
|
||||
if (indexOrButton > -1) {
|
||||
const r = this.buttons.splice(indexOrButton, 1);
|
||||
this.update();
|
||||
return r;
|
||||
const r = this.buttons.splice(indexOrButton, 1)
|
||||
this.update()
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
this.element.replaceChildren(...this.buttons.map((b) => b["element"] ?? b));
|
||||
this.element.replaceChildren(...this.buttons.map((b) => b['element'] ?? b))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export interface ComfyComponent<T extends HTMLElement = HTMLElement> {
|
||||
element: T;
|
||||
element: T
|
||||
}
|
||||
|
||||
@@ -1,140 +1,140 @@
|
||||
import { prop } from "../../utils";
|
||||
import { $el } from "../../ui";
|
||||
import { applyClasses, ClassList } from "../utils";
|
||||
import { prop } from '../../utils'
|
||||
import { $el } from '../../ui'
|
||||
import { applyClasses, ClassList } from '../utils'
|
||||
|
||||
export class ComfyPopup extends EventTarget {
|
||||
element = $el("div.comfyui-popup");
|
||||
open: boolean;
|
||||
children: HTMLElement[];
|
||||
target: HTMLElement;
|
||||
ignoreTarget: boolean;
|
||||
container: HTMLElement;
|
||||
position: string;
|
||||
closeOnEscape: boolean;
|
||||
horizontal: string;
|
||||
classList: ClassList;
|
||||
element = $el('div.comfyui-popup')
|
||||
open: boolean
|
||||
children: HTMLElement[]
|
||||
target: HTMLElement
|
||||
ignoreTarget: boolean
|
||||
container: HTMLElement
|
||||
position: string
|
||||
closeOnEscape: boolean
|
||||
horizontal: string
|
||||
classList: ClassList
|
||||
|
||||
constructor(
|
||||
{
|
||||
target,
|
||||
container = document.body,
|
||||
classList = "",
|
||||
classList = '',
|
||||
ignoreTarget = true,
|
||||
closeOnEscape = true,
|
||||
position = "absolute",
|
||||
horizontal = "left",
|
||||
position = 'absolute',
|
||||
horizontal = 'left'
|
||||
}: {
|
||||
target: HTMLElement;
|
||||
container?: HTMLElement;
|
||||
classList?: ClassList;
|
||||
ignoreTarget?: boolean;
|
||||
closeOnEscape?: boolean;
|
||||
position?: "absolute" | "relative";
|
||||
horizontal?: "left" | "right";
|
||||
target: HTMLElement
|
||||
container?: HTMLElement
|
||||
classList?: ClassList
|
||||
ignoreTarget?: boolean
|
||||
closeOnEscape?: boolean
|
||||
position?: 'absolute' | 'relative'
|
||||
horizontal?: 'left' | 'right'
|
||||
},
|
||||
...children: HTMLElement[]
|
||||
) {
|
||||
super();
|
||||
this.target = target;
|
||||
this.ignoreTarget = ignoreTarget;
|
||||
this.container = container;
|
||||
this.position = position;
|
||||
this.closeOnEscape = closeOnEscape;
|
||||
this.horizontal = horizontal;
|
||||
super()
|
||||
this.target = target
|
||||
this.ignoreTarget = ignoreTarget
|
||||
this.container = container
|
||||
this.position = position
|
||||
this.closeOnEscape = closeOnEscape
|
||||
this.horizontal = horizontal
|
||||
|
||||
container.append(this.element);
|
||||
container.append(this.element)
|
||||
|
||||
this.children = prop(this, "children", children, () => {
|
||||
this.element.replaceChildren(...this.children);
|
||||
this.update();
|
||||
});
|
||||
this.classList = prop(this, "classList", classList, () =>
|
||||
applyClasses(this.element, this.classList, "comfyui-popup", horizontal)
|
||||
);
|
||||
this.open = prop(this, "open", false, (v, o) => {
|
||||
if (v === o) return;
|
||||
this.children = prop(this, 'children', children, () => {
|
||||
this.element.replaceChildren(...this.children)
|
||||
this.update()
|
||||
})
|
||||
this.classList = prop(this, 'classList', classList, () =>
|
||||
applyClasses(this.element, this.classList, 'comfyui-popup', horizontal)
|
||||
)
|
||||
this.open = prop(this, 'open', false, (v, o) => {
|
||||
if (v === o) return
|
||||
if (v) {
|
||||
this.#show();
|
||||
this.#show()
|
||||
} else {
|
||||
this.#hide();
|
||||
this.#hide()
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.open = !this.open;
|
||||
this.open = !this.open
|
||||
}
|
||||
|
||||
#hide() {
|
||||
this.element.classList.remove("open");
|
||||
window.removeEventListener("resize", this.update);
|
||||
window.removeEventListener("click", this.#clickHandler, { capture: true });
|
||||
window.removeEventListener("keydown", this.#escHandler, { capture: true });
|
||||
this.element.classList.remove('open')
|
||||
window.removeEventListener('resize', this.update)
|
||||
window.removeEventListener('click', this.#clickHandler, { capture: true })
|
||||
window.removeEventListener('keydown', this.#escHandler, { capture: true })
|
||||
|
||||
this.dispatchEvent(new CustomEvent("close"));
|
||||
this.dispatchEvent(new CustomEvent("change"));
|
||||
this.dispatchEvent(new CustomEvent('close'))
|
||||
this.dispatchEvent(new CustomEvent('change'))
|
||||
}
|
||||
|
||||
#show() {
|
||||
this.element.classList.add("open");
|
||||
this.update();
|
||||
this.element.classList.add('open')
|
||||
this.update()
|
||||
|
||||
window.addEventListener("resize", this.update);
|
||||
window.addEventListener("click", this.#clickHandler, { capture: true });
|
||||
window.addEventListener('resize', this.update)
|
||||
window.addEventListener('click', this.#clickHandler, { capture: true })
|
||||
if (this.closeOnEscape) {
|
||||
window.addEventListener("keydown", this.#escHandler, { capture: true });
|
||||
window.addEventListener('keydown', this.#escHandler, { capture: true })
|
||||
}
|
||||
|
||||
this.dispatchEvent(new CustomEvent("open"));
|
||||
this.dispatchEvent(new CustomEvent("change"));
|
||||
this.dispatchEvent(new CustomEvent('open'))
|
||||
this.dispatchEvent(new CustomEvent('change'))
|
||||
}
|
||||
|
||||
#escHandler = (e) => {
|
||||
if (e.key === "Escape") {
|
||||
this.open = false;
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
if (e.key === 'Escape') {
|
||||
this.open = false
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#clickHandler = (e) => {
|
||||
/** @type {any} */
|
||||
const target = e.target;
|
||||
const target = e.target
|
||||
if (
|
||||
!this.element.contains(target) &&
|
||||
this.ignoreTarget &&
|
||||
!this.target.contains(target)
|
||||
) {
|
||||
this.open = false;
|
||||
this.open = false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
update = () => {
|
||||
const rect = this.target.getBoundingClientRect();
|
||||
this.element.style.setProperty("--bottom", "unset");
|
||||
if (this.position === "absolute") {
|
||||
if (this.horizontal === "left") {
|
||||
this.element.style.setProperty("--left", rect.left + "px");
|
||||
const rect = this.target.getBoundingClientRect()
|
||||
this.element.style.setProperty('--bottom', 'unset')
|
||||
if (this.position === 'absolute') {
|
||||
if (this.horizontal === 'left') {
|
||||
this.element.style.setProperty('--left', rect.left + 'px')
|
||||
} else {
|
||||
this.element.style.setProperty(
|
||||
"--left",
|
||||
rect.right - this.element.clientWidth + "px"
|
||||
);
|
||||
'--left',
|
||||
rect.right - this.element.clientWidth + 'px'
|
||||
)
|
||||
}
|
||||
this.element.style.setProperty("--top", rect.bottom + "px");
|
||||
this.element.style.setProperty("--limit", rect.bottom + "px");
|
||||
this.element.style.setProperty('--top', rect.bottom + 'px')
|
||||
this.element.style.setProperty('--limit', rect.bottom + 'px')
|
||||
} else {
|
||||
this.element.style.setProperty("--left", 0 + "px");
|
||||
this.element.style.setProperty("--top", rect.height + "px");
|
||||
this.element.style.setProperty("--limit", rect.height + "px");
|
||||
this.element.style.setProperty('--left', 0 + 'px')
|
||||
this.element.style.setProperty('--top', rect.height + 'px')
|
||||
this.element.style.setProperty('--limit', rect.height + 'px')
|
||||
}
|
||||
|
||||
const thisRect = this.element.getBoundingClientRect();
|
||||
const thisRect = this.element.getBoundingClientRect()
|
||||
if (thisRect.height < 30) {
|
||||
// Move up instead
|
||||
this.element.style.setProperty("--top", "unset");
|
||||
this.element.style.setProperty("--bottom", rect.height + 5 + "px");
|
||||
this.element.style.setProperty("--limit", rect.height + 5 + "px");
|
||||
this.element.style.setProperty('--top', 'unset')
|
||||
this.element.style.setProperty('--bottom', rect.height + 5 + 'px')
|
||||
this.element.style.setProperty('--limit', rect.height + 5 + 'px')
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
import { $el } from "../../ui";
|
||||
import { ComfyButton } from "./button";
|
||||
import { prop } from "../../utils";
|
||||
import { ComfyPopup } from "./popup";
|
||||
import { $el } from '../../ui'
|
||||
import { ComfyButton } from './button'
|
||||
import { prop } from '../../utils'
|
||||
import { ComfyPopup } from './popup'
|
||||
|
||||
export class ComfySplitButton {
|
||||
arrow: ComfyButton;
|
||||
element: HTMLElement;
|
||||
popup: ComfyPopup;
|
||||
items: Array<HTMLElement | ComfyButton>;
|
||||
arrow: ComfyButton
|
||||
element: HTMLElement
|
||||
popup: ComfyPopup
|
||||
items: Array<HTMLElement | ComfyButton>
|
||||
|
||||
constructor(
|
||||
{
|
||||
primary,
|
||||
mode,
|
||||
horizontal = "left",
|
||||
position = "relative",
|
||||
horizontal = 'left',
|
||||
position = 'relative'
|
||||
}: {
|
||||
primary: ComfyButton;
|
||||
mode?: "hover" | "click";
|
||||
horizontal?: "left" | "right";
|
||||
position?: "relative" | "absolute";
|
||||
primary: ComfyButton
|
||||
mode?: 'hover' | 'click'
|
||||
horizontal?: 'left' | 'right'
|
||||
position?: 'relative' | 'absolute'
|
||||
},
|
||||
...items: Array<HTMLElement | ComfyButton>
|
||||
) {
|
||||
this.arrow = new ComfyButton({
|
||||
icon: "chevron-down",
|
||||
});
|
||||
icon: 'chevron-down'
|
||||
})
|
||||
this.element = $el(
|
||||
"div.comfyui-split-button" + (mode === "hover" ? ".hover" : ""),
|
||||
'div.comfyui-split-button' + (mode === 'hover' ? '.hover' : ''),
|
||||
[
|
||||
$el("div.comfyui-split-primary", primary.element),
|
||||
$el("div.comfyui-split-arrow", this.arrow.element),
|
||||
$el('div.comfyui-split-primary', primary.element),
|
||||
$el('div.comfyui-split-arrow', this.arrow.element)
|
||||
]
|
||||
);
|
||||
)
|
||||
this.popup = new ComfyPopup({
|
||||
target: this.element,
|
||||
container: position === "relative" ? this.element : document.body,
|
||||
container: position === 'relative' ? this.element : document.body,
|
||||
classList:
|
||||
"comfyui-split-button-popup" + (mode === "hover" ? " hover" : ""),
|
||||
closeOnEscape: mode === "click",
|
||||
'comfyui-split-button-popup' + (mode === 'hover' ? ' hover' : ''),
|
||||
closeOnEscape: mode === 'click',
|
||||
position,
|
||||
horizontal,
|
||||
});
|
||||
horizontal
|
||||
})
|
||||
|
||||
this.arrow.withPopup(this.popup, mode);
|
||||
this.arrow.withPopup(this.popup, mode)
|
||||
|
||||
this.items = prop(this, "items", items, () => this.update());
|
||||
this.items = prop(this, 'items', items, () => this.update())
|
||||
}
|
||||
|
||||
update() {
|
||||
this.popup.element.replaceChildren(
|
||||
...this.items.map((b) => ("element" in b ? b.element : b))
|
||||
);
|
||||
...this.items.map((b) => ('element' in b ? b.element : b))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
import { $el } from "../ui";
|
||||
import { $el } from '../ui'
|
||||
|
||||
export class ComfyDialog<
|
||||
T extends HTMLElement = HTMLElement,
|
||||
T extends HTMLElement = HTMLElement
|
||||
> extends EventTarget {
|
||||
element: T;
|
||||
textElement: HTMLElement;
|
||||
#buttons: HTMLButtonElement[] | null;
|
||||
element: T
|
||||
textElement: HTMLElement
|
||||
#buttons: HTMLButtonElement[] | null
|
||||
|
||||
constructor(type = "div", buttons = null) {
|
||||
super();
|
||||
this.#buttons = buttons;
|
||||
this.element = $el(type + ".comfy-modal", { parent: document.body }, [
|
||||
$el("div.comfy-modal-content", [
|
||||
$el("p", { $: (p) => (this.textElement = p) }),
|
||||
...this.createButtons(),
|
||||
]),
|
||||
]) as T;
|
||||
constructor(type = 'div', buttons = null) {
|
||||
super()
|
||||
this.#buttons = buttons
|
||||
this.element = $el(type + '.comfy-modal', { parent: document.body }, [
|
||||
$el('div.comfy-modal-content', [
|
||||
$el('p', { $: (p) => (this.textElement = p) }),
|
||||
...this.createButtons()
|
||||
])
|
||||
]) as T
|
||||
}
|
||||
|
||||
createButtons() {
|
||||
return (
|
||||
this.#buttons ?? [
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Close",
|
||||
onclick: () => this.close(),
|
||||
}),
|
||||
$el('button', {
|
||||
type: 'button',
|
||||
textContent: 'Close',
|
||||
onclick: () => this.close()
|
||||
})
|
||||
]
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
close() {
|
||||
this.element.style.display = "none";
|
||||
this.element.style.display = 'none'
|
||||
}
|
||||
|
||||
show(html) {
|
||||
if (typeof html === "string") {
|
||||
this.textElement.innerHTML = html;
|
||||
if (typeof html === 'string') {
|
||||
this.textElement.innerHTML = html
|
||||
} else {
|
||||
this.textElement.replaceChildren(
|
||||
...(html instanceof Array ? html : [html])
|
||||
);
|
||||
)
|
||||
}
|
||||
this.element.style.display = "flex";
|
||||
this.element.style.display = 'flex'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
import { $el } from "../ui";
|
||||
import { $el } from '../ui'
|
||||
|
||||
$el("style", {
|
||||
$el('style', {
|
||||
parent: document.head,
|
||||
textContent: `
|
||||
.draggable-item {
|
||||
@@ -40,261 +40,261 @@ $el("style", {
|
||||
.draggable-item.is-draggable {
|
||||
z-index: 10;
|
||||
}
|
||||
`,
|
||||
});
|
||||
`
|
||||
})
|
||||
|
||||
export class DraggableList extends EventTarget {
|
||||
listContainer;
|
||||
draggableItem;
|
||||
pointerStartX;
|
||||
pointerStartY;
|
||||
scrollYMax;
|
||||
itemsGap = 0;
|
||||
items = [];
|
||||
itemSelector;
|
||||
handleClass = "drag-handle";
|
||||
off = [];
|
||||
offDrag = [];
|
||||
listContainer
|
||||
draggableItem
|
||||
pointerStartX
|
||||
pointerStartY
|
||||
scrollYMax
|
||||
itemsGap = 0
|
||||
items = []
|
||||
itemSelector
|
||||
handleClass = 'drag-handle'
|
||||
off = []
|
||||
offDrag = []
|
||||
|
||||
constructor(element, itemSelector) {
|
||||
super();
|
||||
this.listContainer = element;
|
||||
this.itemSelector = itemSelector;
|
||||
super()
|
||||
this.listContainer = element
|
||||
this.itemSelector = itemSelector
|
||||
|
||||
if (!this.listContainer) return;
|
||||
if (!this.listContainer) return
|
||||
|
||||
this.off.push(this.on(this.listContainer, "mousedown", this.dragStart));
|
||||
this.off.push(this.on(this.listContainer, "touchstart", this.dragStart));
|
||||
this.off.push(this.on(document, "mouseup", this.dragEnd));
|
||||
this.off.push(this.on(document, "touchend", this.dragEnd));
|
||||
this.off.push(this.on(this.listContainer, 'mousedown', this.dragStart))
|
||||
this.off.push(this.on(this.listContainer, 'touchstart', this.dragStart))
|
||||
this.off.push(this.on(document, 'mouseup', this.dragEnd))
|
||||
this.off.push(this.on(document, 'touchend', this.dragEnd))
|
||||
}
|
||||
|
||||
getAllItems() {
|
||||
if (!this.items?.length) {
|
||||
this.items = Array.from(
|
||||
this.listContainer.querySelectorAll(this.itemSelector)
|
||||
);
|
||||
)
|
||||
this.items.forEach((element) => {
|
||||
element.classList.add("is-idle");
|
||||
});
|
||||
element.classList.add('is-idle')
|
||||
})
|
||||
}
|
||||
return this.items;
|
||||
return this.items
|
||||
}
|
||||
|
||||
getIdleItems() {
|
||||
return this.getAllItems().filter((item) =>
|
||||
item.classList.contains("is-idle")
|
||||
);
|
||||
item.classList.contains('is-idle')
|
||||
)
|
||||
}
|
||||
|
||||
isItemAbove(item) {
|
||||
return item.hasAttribute("data-is-above");
|
||||
return item.hasAttribute('data-is-above')
|
||||
}
|
||||
|
||||
isItemToggled(item) {
|
||||
return item.hasAttribute("data-is-toggled");
|
||||
return item.hasAttribute('data-is-toggled')
|
||||
}
|
||||
|
||||
on(source, event, listener, options?) {
|
||||
listener = listener.bind(this);
|
||||
source.addEventListener(event, listener, options);
|
||||
return () => source.removeEventListener(event, listener);
|
||||
listener = listener.bind(this)
|
||||
source.addEventListener(event, listener, options)
|
||||
return () => source.removeEventListener(event, listener)
|
||||
}
|
||||
|
||||
dragStart(e) {
|
||||
if (e.target.classList.contains(this.handleClass)) {
|
||||
this.draggableItem = e.target.closest(this.itemSelector);
|
||||
this.draggableItem = e.target.closest(this.itemSelector)
|
||||
}
|
||||
|
||||
if (!this.draggableItem) return;
|
||||
if (!this.draggableItem) return
|
||||
|
||||
this.pointerStartX = e.clientX || e.touches[0].clientX;
|
||||
this.pointerStartY = e.clientY || e.touches[0].clientY;
|
||||
this.pointerStartX = e.clientX || e.touches[0].clientX
|
||||
this.pointerStartY = e.clientY || e.touches[0].clientY
|
||||
this.scrollYMax =
|
||||
this.listContainer.scrollHeight - this.listContainer.clientHeight;
|
||||
this.listContainer.scrollHeight - this.listContainer.clientHeight
|
||||
|
||||
this.setItemsGap();
|
||||
this.initDraggableItem();
|
||||
this.initItemsState();
|
||||
this.setItemsGap()
|
||||
this.initDraggableItem()
|
||||
this.initItemsState()
|
||||
|
||||
this.offDrag.push(this.on(document, "mousemove", this.drag));
|
||||
this.offDrag.push(this.on(document, 'mousemove', this.drag))
|
||||
this.offDrag.push(
|
||||
this.on(document, "touchmove", this.drag, { passive: false })
|
||||
);
|
||||
this.on(document, 'touchmove', this.drag, { passive: false })
|
||||
)
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("dragstart", {
|
||||
new CustomEvent('dragstart', {
|
||||
detail: {
|
||||
element: this.draggableItem,
|
||||
position: this.getAllItems().indexOf(this.draggableItem),
|
||||
},
|
||||
position: this.getAllItems().indexOf(this.draggableItem)
|
||||
}
|
||||
})
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
setItemsGap() {
|
||||
if (this.getIdleItems().length <= 1) {
|
||||
this.itemsGap = 0;
|
||||
return;
|
||||
this.itemsGap = 0
|
||||
return
|
||||
}
|
||||
|
||||
const item1 = this.getIdleItems()[0];
|
||||
const item2 = this.getIdleItems()[1];
|
||||
const item1 = this.getIdleItems()[0]
|
||||
const item2 = this.getIdleItems()[1]
|
||||
|
||||
const item1Rect = item1.getBoundingClientRect();
|
||||
const item2Rect = item2.getBoundingClientRect();
|
||||
const item1Rect = item1.getBoundingClientRect()
|
||||
const item2Rect = item2.getBoundingClientRect()
|
||||
|
||||
this.itemsGap = Math.abs(item1Rect.bottom - item2Rect.top);
|
||||
this.itemsGap = Math.abs(item1Rect.bottom - item2Rect.top)
|
||||
}
|
||||
|
||||
initItemsState() {
|
||||
this.getIdleItems().forEach((item, i) => {
|
||||
if (this.getAllItems().indexOf(this.draggableItem) > i) {
|
||||
item.dataset.isAbove = "";
|
||||
item.dataset.isAbove = ''
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
initDraggableItem() {
|
||||
this.draggableItem.classList.remove("is-idle");
|
||||
this.draggableItem.classList.add("is-draggable");
|
||||
this.draggableItem.classList.remove('is-idle')
|
||||
this.draggableItem.classList.add('is-draggable')
|
||||
}
|
||||
|
||||
drag(e) {
|
||||
if (!this.draggableItem) return;
|
||||
if (!this.draggableItem) return
|
||||
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
const clientX = e.clientX || e.touches[0].clientX;
|
||||
const clientY = e.clientY || e.touches[0].clientY;
|
||||
const clientX = e.clientX || e.touches[0].clientX
|
||||
const clientY = e.clientY || e.touches[0].clientY
|
||||
|
||||
const listRect = this.listContainer.getBoundingClientRect();
|
||||
const listRect = this.listContainer.getBoundingClientRect()
|
||||
|
||||
if (clientY > listRect.bottom) {
|
||||
if (this.listContainer.scrollTop < this.scrollYMax) {
|
||||
this.listContainer.scrollBy(0, 10);
|
||||
this.pointerStartY -= 10;
|
||||
this.listContainer.scrollBy(0, 10)
|
||||
this.pointerStartY -= 10
|
||||
}
|
||||
} else if (clientY < listRect.top && this.listContainer.scrollTop > 0) {
|
||||
this.pointerStartY += 10;
|
||||
this.listContainer.scrollBy(0, -10);
|
||||
this.pointerStartY += 10
|
||||
this.listContainer.scrollBy(0, -10)
|
||||
}
|
||||
|
||||
const pointerOffsetX = clientX - this.pointerStartX;
|
||||
const pointerOffsetY = clientY - this.pointerStartY;
|
||||
const pointerOffsetX = clientX - this.pointerStartX
|
||||
const pointerOffsetY = clientY - this.pointerStartY
|
||||
|
||||
this.updateIdleItemsStateAndPosition();
|
||||
this.draggableItem.style.transform = `translate(${pointerOffsetX}px, ${pointerOffsetY}px)`;
|
||||
this.updateIdleItemsStateAndPosition()
|
||||
this.draggableItem.style.transform = `translate(${pointerOffsetX}px, ${pointerOffsetY}px)`
|
||||
}
|
||||
|
||||
updateIdleItemsStateAndPosition() {
|
||||
const draggableItemRect = this.draggableItem.getBoundingClientRect();
|
||||
const draggableItemY = draggableItemRect.top + draggableItemRect.height / 2;
|
||||
const draggableItemRect = this.draggableItem.getBoundingClientRect()
|
||||
const draggableItemY = draggableItemRect.top + draggableItemRect.height / 2
|
||||
|
||||
// Update state
|
||||
this.getIdleItems().forEach((item) => {
|
||||
const itemRect = item.getBoundingClientRect();
|
||||
const itemY = itemRect.top + itemRect.height / 2;
|
||||
const itemRect = item.getBoundingClientRect()
|
||||
const itemY = itemRect.top + itemRect.height / 2
|
||||
if (this.isItemAbove(item)) {
|
||||
if (draggableItemY <= itemY) {
|
||||
item.dataset.isToggled = "";
|
||||
item.dataset.isToggled = ''
|
||||
} else {
|
||||
delete item.dataset.isToggled;
|
||||
delete item.dataset.isToggled
|
||||
}
|
||||
} else {
|
||||
if (draggableItemY >= itemY) {
|
||||
item.dataset.isToggled = "";
|
||||
item.dataset.isToggled = ''
|
||||
} else {
|
||||
delete item.dataset.isToggled;
|
||||
delete item.dataset.isToggled
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// Update position
|
||||
this.getIdleItems().forEach((item) => {
|
||||
if (this.isItemToggled(item)) {
|
||||
const direction = this.isItemAbove(item) ? 1 : -1;
|
||||
item.style.transform = `translateY(${direction * (draggableItemRect.height + this.itemsGap)}px)`;
|
||||
const direction = this.isItemAbove(item) ? 1 : -1
|
||||
item.style.transform = `translateY(${direction * (draggableItemRect.height + this.itemsGap)}px)`
|
||||
} else {
|
||||
item.style.transform = "";
|
||||
item.style.transform = ''
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
dragEnd() {
|
||||
if (!this.draggableItem) return;
|
||||
if (!this.draggableItem) return
|
||||
|
||||
this.applyNewItemsOrder();
|
||||
this.cleanup();
|
||||
this.applyNewItemsOrder()
|
||||
this.cleanup()
|
||||
}
|
||||
|
||||
applyNewItemsOrder() {
|
||||
const reorderedItems = [];
|
||||
const reorderedItems = []
|
||||
|
||||
let oldPosition = -1;
|
||||
let oldPosition = -1
|
||||
this.getAllItems().forEach((item, index) => {
|
||||
if (item === this.draggableItem) {
|
||||
oldPosition = index;
|
||||
return;
|
||||
oldPosition = index
|
||||
return
|
||||
}
|
||||
if (!this.isItemToggled(item)) {
|
||||
reorderedItems[index] = item;
|
||||
return;
|
||||
reorderedItems[index] = item
|
||||
return
|
||||
}
|
||||
const newIndex = this.isItemAbove(item) ? index + 1 : index - 1;
|
||||
reorderedItems[newIndex] = item;
|
||||
});
|
||||
const newIndex = this.isItemAbove(item) ? index + 1 : index - 1
|
||||
reorderedItems[newIndex] = item
|
||||
})
|
||||
|
||||
for (let index = 0; index < this.getAllItems().length; index++) {
|
||||
const item = reorderedItems[index];
|
||||
if (typeof item === "undefined") {
|
||||
reorderedItems[index] = this.draggableItem;
|
||||
const item = reorderedItems[index]
|
||||
if (typeof item === 'undefined') {
|
||||
reorderedItems[index] = this.draggableItem
|
||||
}
|
||||
}
|
||||
|
||||
reorderedItems.forEach((item) => {
|
||||
this.listContainer.appendChild(item);
|
||||
});
|
||||
this.listContainer.appendChild(item)
|
||||
})
|
||||
|
||||
this.items = reorderedItems;
|
||||
this.items = reorderedItems
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("dragend", {
|
||||
new CustomEvent('dragend', {
|
||||
detail: {
|
||||
element: this.draggableItem,
|
||||
oldPosition,
|
||||
newPosition: reorderedItems.indexOf(this.draggableItem),
|
||||
},
|
||||
newPosition: reorderedItems.indexOf(this.draggableItem)
|
||||
}
|
||||
})
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.itemsGap = 0;
|
||||
this.items = [];
|
||||
this.unsetDraggableItem();
|
||||
this.unsetItemState();
|
||||
this.itemsGap = 0
|
||||
this.items = []
|
||||
this.unsetDraggableItem()
|
||||
this.unsetItemState()
|
||||
|
||||
this.offDrag.forEach((f) => f());
|
||||
this.offDrag = [];
|
||||
this.offDrag.forEach((f) => f())
|
||||
this.offDrag = []
|
||||
}
|
||||
|
||||
unsetDraggableItem() {
|
||||
this.draggableItem.style = null;
|
||||
this.draggableItem.classList.remove("is-draggable");
|
||||
this.draggableItem.classList.add("is-idle");
|
||||
this.draggableItem = null;
|
||||
this.draggableItem.style = null
|
||||
this.draggableItem.classList.remove('is-draggable')
|
||||
this.draggableItem.classList.add('is-idle')
|
||||
this.draggableItem = null
|
||||
}
|
||||
|
||||
unsetItemState() {
|
||||
this.getIdleItems().forEach((item, i) => {
|
||||
delete item.dataset.isAbove;
|
||||
delete item.dataset.isToggled;
|
||||
item.style.transform = "";
|
||||
});
|
||||
delete item.dataset.isAbove
|
||||
delete item.dataset.isToggled
|
||||
item.style.transform = ''
|
||||
})
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.off.forEach((f) => f());
|
||||
this.off.forEach((f) => f())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +1,72 @@
|
||||
import { app } from "../app";
|
||||
import { $el } from "../ui";
|
||||
import { app } from '../app'
|
||||
import { $el } from '../ui'
|
||||
|
||||
export function calculateImageGrid(imgs, dw, dh) {
|
||||
let best = 0;
|
||||
let w = imgs[0].naturalWidth;
|
||||
let h = imgs[0].naturalHeight;
|
||||
const numImages = imgs.length;
|
||||
let best = 0
|
||||
let w = imgs[0].naturalWidth
|
||||
let h = imgs[0].naturalHeight
|
||||
const numImages = imgs.length
|
||||
|
||||
let cellWidth, cellHeight, cols, rows, shiftX;
|
||||
let cellWidth, cellHeight, cols, rows, shiftX
|
||||
// compact style
|
||||
for (let c = 1; c <= numImages; c++) {
|
||||
const r = Math.ceil(numImages / c);
|
||||
const cW = dw / c;
|
||||
const cH = dh / r;
|
||||
const scaleX = cW / w;
|
||||
const scaleY = cH / h;
|
||||
const r = Math.ceil(numImages / c)
|
||||
const cW = dw / c
|
||||
const cH = dh / r
|
||||
const scaleX = cW / w
|
||||
const scaleY = cH / h
|
||||
|
||||
const scale = Math.min(scaleX, scaleY, 1);
|
||||
const imageW = w * scale;
|
||||
const imageH = h * scale;
|
||||
const area = imageW * imageH * numImages;
|
||||
const scale = Math.min(scaleX, scaleY, 1)
|
||||
const imageW = w * scale
|
||||
const imageH = h * scale
|
||||
const area = imageW * imageH * numImages
|
||||
|
||||
if (area > best) {
|
||||
best = area;
|
||||
cellWidth = imageW;
|
||||
cellHeight = imageH;
|
||||
cols = c;
|
||||
rows = r;
|
||||
shiftX = c * ((cW - imageW) / 2);
|
||||
best = area
|
||||
cellWidth = imageW
|
||||
cellHeight = imageH
|
||||
cols = c
|
||||
rows = r
|
||||
shiftX = c * ((cW - imageW) / 2)
|
||||
}
|
||||
}
|
||||
|
||||
return { cellWidth, cellHeight, cols, rows, shiftX };
|
||||
return { cellWidth, cellHeight, cols, rows, shiftX }
|
||||
}
|
||||
|
||||
export function createImageHost(node) {
|
||||
const el = $el("div.comfy-img-preview");
|
||||
let currentImgs;
|
||||
let first = true;
|
||||
const el = $el('div.comfy-img-preview')
|
||||
let currentImgs
|
||||
let first = true
|
||||
|
||||
function updateSize() {
|
||||
let w = null;
|
||||
let h = null;
|
||||
let w = null
|
||||
let h = null
|
||||
|
||||
if (currentImgs) {
|
||||
let elH = el.clientHeight;
|
||||
let elH = el.clientHeight
|
||||
if (first) {
|
||||
first = false;
|
||||
first = false
|
||||
// On first run, if we are small then grow a bit
|
||||
if (elH < 190) {
|
||||
elH = 190;
|
||||
elH = 190
|
||||
}
|
||||
el.style.setProperty("--comfy-widget-min-height", elH.toString());
|
||||
el.style.setProperty('--comfy-widget-min-height', elH.toString())
|
||||
} else {
|
||||
el.style.setProperty("--comfy-widget-min-height", null);
|
||||
el.style.setProperty('--comfy-widget-min-height', null)
|
||||
}
|
||||
|
||||
const nw = node.size[0];
|
||||
({ cellWidth: w, cellHeight: h } = calculateImageGrid(
|
||||
const nw = node.size[0]
|
||||
;({ cellWidth: w, cellHeight: h } = calculateImageGrid(
|
||||
currentImgs,
|
||||
nw - 20,
|
||||
elH
|
||||
));
|
||||
w += "px";
|
||||
h += "px";
|
||||
))
|
||||
w += 'px'
|
||||
h += 'px'
|
||||
|
||||
el.style.setProperty("--comfy-img-preview-width", w);
|
||||
el.style.setProperty("--comfy-img-preview-height", h);
|
||||
el.style.setProperty('--comfy-img-preview-width', w)
|
||||
el.style.setProperty('--comfy-img-preview-height', h)
|
||||
}
|
||||
}
|
||||
return {
|
||||
@@ -75,31 +75,31 @@ export function createImageHost(node) {
|
||||
if (imgs !== currentImgs) {
|
||||
if (currentImgs == null) {
|
||||
requestAnimationFrame(() => {
|
||||
updateSize();
|
||||
});
|
||||
updateSize()
|
||||
})
|
||||
}
|
||||
el.replaceChildren(...imgs);
|
||||
currentImgs = imgs;
|
||||
node.onResize(node.size);
|
||||
node.graph.setDirtyCanvas(true, true);
|
||||
el.replaceChildren(...imgs)
|
||||
currentImgs = imgs
|
||||
node.onResize(node.size)
|
||||
node.graph.setDirtyCanvas(true, true)
|
||||
}
|
||||
},
|
||||
getHeight() {
|
||||
updateSize();
|
||||
updateSize()
|
||||
},
|
||||
onDraw() {
|
||||
// Element from point uses a hittest find elements so we need to toggle pointer events
|
||||
el.style.pointerEvents = "all";
|
||||
el.style.pointerEvents = 'all'
|
||||
const over = document.elementFromPoint(
|
||||
app.canvas.mouse[0],
|
||||
app.canvas.mouse[1]
|
||||
);
|
||||
el.style.pointerEvents = "none";
|
||||
)
|
||||
el.style.pointerEvents = 'none'
|
||||
|
||||
if (!over) return;
|
||||
if (!over) return
|
||||
// Set the overIndex so Open Image etc work
|
||||
const idx = currentImgs.indexOf(over);
|
||||
node.overIndex = idx;
|
||||
},
|
||||
};
|
||||
const idx = currentImgs.indexOf(over)
|
||||
node.overIndex = idx
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,320 +1,308 @@
|
||||
import type { ComfyApp } from "@/scripts/app";
|
||||
import { api } from "../../api";
|
||||
import { $el } from "../../ui";
|
||||
import { downloadBlob } from "../../utils";
|
||||
import { ComfyButton } from "../components/button";
|
||||
import { ComfyButtonGroup } from "../components/buttonGroup";
|
||||
import { ComfySplitButton } from "../components/splitButton";
|
||||
import { ComfyQueueButton } from "./queueButton";
|
||||
import { ComfyWorkflowsMenu } from "./workflows";
|
||||
import { getInteruptButton } from "./interruptButton";
|
||||
import "./menu.css";
|
||||
import type { ComfySettingsDialog } from "../settings";
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
import { api } from '../../api'
|
||||
import { $el } from '../../ui'
|
||||
import { downloadBlob } from '../../utils'
|
||||
import { ComfyButton } from '../components/button'
|
||||
import { ComfyButtonGroup } from '../components/buttonGroup'
|
||||
import { ComfySplitButton } from '../components/splitButton'
|
||||
import { ComfyQueueButton } from './queueButton'
|
||||
import { ComfyWorkflowsMenu } from './workflows'
|
||||
import { getInteruptButton } from './interruptButton'
|
||||
import './menu.css'
|
||||
import type { ComfySettingsDialog } from '../settings'
|
||||
|
||||
type MenuPosition = "Disabled" | "Top" | "Bottom";
|
||||
type MenuPosition = 'Disabled' | 'Top' | 'Bottom'
|
||||
|
||||
const collapseOnMobile = (t) => {
|
||||
(t.element ?? t).classList.add("comfyui-menu-mobile-collapse");
|
||||
return t;
|
||||
};
|
||||
;(t.element ?? t).classList.add('comfyui-menu-mobile-collapse')
|
||||
return t
|
||||
}
|
||||
const showOnMobile = (t) => {
|
||||
(t.element ?? t).classList.add("lt-lg-show");
|
||||
return t;
|
||||
};
|
||||
;(t.element ?? t).classList.add('lt-lg-show')
|
||||
return t
|
||||
}
|
||||
|
||||
export class ComfyAppMenu {
|
||||
#sizeBreak = "lg";
|
||||
#sizeBreak = 'lg'
|
||||
#lastSizeBreaks = {
|
||||
lg: null,
|
||||
md: null,
|
||||
sm: null,
|
||||
xs: null,
|
||||
};
|
||||
#sizeBreaks = Object.keys(this.#lastSizeBreaks);
|
||||
#cachedInnerSize = null;
|
||||
#cacheTimeout = null;
|
||||
app: ComfyApp;
|
||||
workflows: ComfyWorkflowsMenu;
|
||||
logo: HTMLElement;
|
||||
saveButton: ComfySplitButton;
|
||||
actionsGroup: ComfyButtonGroup;
|
||||
settingsGroup: ComfyButtonGroup;
|
||||
viewGroup: ComfyButtonGroup;
|
||||
mobileMenuButton: ComfyButton;
|
||||
element: HTMLElement;
|
||||
menuPositionSetting: ReturnType<ComfySettingsDialog["addSetting"]>;
|
||||
position: MenuPosition;
|
||||
xs: null
|
||||
}
|
||||
#sizeBreaks = Object.keys(this.#lastSizeBreaks)
|
||||
#cachedInnerSize = null
|
||||
#cacheTimeout = null
|
||||
app: ComfyApp
|
||||
workflows: ComfyWorkflowsMenu
|
||||
logo: HTMLElement
|
||||
saveButton: ComfySplitButton
|
||||
actionsGroup: ComfyButtonGroup
|
||||
settingsGroup: ComfyButtonGroup
|
||||
viewGroup: ComfyButtonGroup
|
||||
mobileMenuButton: ComfyButton
|
||||
element: HTMLElement
|
||||
menuPositionSetting: ReturnType<ComfySettingsDialog['addSetting']>
|
||||
position: MenuPosition
|
||||
|
||||
constructor(app: ComfyApp) {
|
||||
this.app = app;
|
||||
this.app = app
|
||||
|
||||
this.workflows = new ComfyWorkflowsMenu(app);
|
||||
this.workflows = new ComfyWorkflowsMenu(app)
|
||||
const getSaveButton = (t?: string) =>
|
||||
new ComfyButton({
|
||||
icon: "content-save",
|
||||
tooltip: "Save the current workflow",
|
||||
icon: 'content-save',
|
||||
tooltip: 'Save the current workflow',
|
||||
action: () => app.workflowManager.activeWorkflow.save(),
|
||||
content: t,
|
||||
});
|
||||
content: t
|
||||
})
|
||||
|
||||
this.logo = $el(
|
||||
"h1.comfyui-logo.nlg-hide",
|
||||
{ title: "ComfyUI" },
|
||||
"ComfyUI"
|
||||
);
|
||||
this.logo = $el('h1.comfyui-logo.nlg-hide', { title: 'ComfyUI' }, 'ComfyUI')
|
||||
this.saveButton = new ComfySplitButton(
|
||||
{
|
||||
primary: getSaveButton(),
|
||||
mode: "hover",
|
||||
position: "absolute",
|
||||
mode: 'hover',
|
||||
position: 'absolute'
|
||||
},
|
||||
getSaveButton("Save"),
|
||||
getSaveButton('Save'),
|
||||
new ComfyButton({
|
||||
icon: "content-save-edit",
|
||||
content: "Save As",
|
||||
tooltip: "Save the current graph as a new workflow",
|
||||
action: () => app.workflowManager.activeWorkflow.save(true),
|
||||
icon: 'content-save-edit',
|
||||
content: 'Save As',
|
||||
tooltip: 'Save the current graph as a new workflow',
|
||||
action: () => app.workflowManager.activeWorkflow.save(true)
|
||||
}),
|
||||
new ComfyButton({
|
||||
icon: "download",
|
||||
content: "Export",
|
||||
tooltip: "Export the current workflow as JSON",
|
||||
action: () => this.exportWorkflow("workflow", "workflow"),
|
||||
icon: 'download',
|
||||
content: 'Export',
|
||||
tooltip: 'Export the current workflow as JSON',
|
||||
action: () => this.exportWorkflow('workflow', 'workflow')
|
||||
}),
|
||||
new ComfyButton({
|
||||
icon: "api",
|
||||
content: "Export (API Format)",
|
||||
icon: 'api',
|
||||
content: 'Export (API Format)',
|
||||
tooltip:
|
||||
"Export the current workflow as JSON for use with the ComfyUI API",
|
||||
action: () => this.exportWorkflow("workflow_api", "output"),
|
||||
visibilitySetting: { id: "Comfy.DevMode", showValue: true },
|
||||
app,
|
||||
'Export the current workflow as JSON for use with the ComfyUI API',
|
||||
action: () => this.exportWorkflow('workflow_api', 'output'),
|
||||
visibilitySetting: { id: 'Comfy.DevMode', showValue: true },
|
||||
app
|
||||
})
|
||||
);
|
||||
)
|
||||
this.actionsGroup = new ComfyButtonGroup(
|
||||
new ComfyButton({
|
||||
icon: "refresh",
|
||||
content: "Refresh",
|
||||
tooltip: "Refresh widgets in nodes to find new models or files",
|
||||
action: () => app.refreshComboInNodes(),
|
||||
icon: 'refresh',
|
||||
content: 'Refresh',
|
||||
tooltip: 'Refresh widgets in nodes to find new models or files',
|
||||
action: () => app.refreshComboInNodes()
|
||||
}),
|
||||
new ComfyButton({
|
||||
icon: "clipboard-edit-outline",
|
||||
content: "Clipspace",
|
||||
tooltip: "Open Clipspace window",
|
||||
action: () => app["openClipspace"](),
|
||||
icon: 'clipboard-edit-outline',
|
||||
content: 'Clipspace',
|
||||
tooltip: 'Open Clipspace window',
|
||||
action: () => app['openClipspace']()
|
||||
}),
|
||||
new ComfyButton({
|
||||
icon: "fit-to-page-outline",
|
||||
content: "Reset View",
|
||||
tooltip: "Reset the canvas view",
|
||||
action: () => app.resetView(),
|
||||
icon: 'fit-to-page-outline',
|
||||
content: 'Reset View',
|
||||
tooltip: 'Reset the canvas view',
|
||||
action: () => app.resetView()
|
||||
}),
|
||||
new ComfyButton({
|
||||
icon: "cancel",
|
||||
content: "Clear",
|
||||
tooltip: "Clears current workflow",
|
||||
icon: 'cancel',
|
||||
content: 'Clear',
|
||||
tooltip: 'Clears current workflow',
|
||||
action: () => {
|
||||
if (
|
||||
!app.ui.settings.getSettingValue("Comfy.ConfirmClear", true) ||
|
||||
confirm("Clear workflow?")
|
||||
!app.ui.settings.getSettingValue('Comfy.ConfirmClear', true) ||
|
||||
confirm('Clear workflow?')
|
||||
) {
|
||||
app.clean();
|
||||
app.graph.clear();
|
||||
api.dispatchEvent(new CustomEvent("graphCleared"));
|
||||
app.clean()
|
||||
app.graph.clear()
|
||||
api.dispatchEvent(new CustomEvent('graphCleared'))
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
)
|
||||
// Keep the settings group as there are custom scripts attaching extra
|
||||
// elements to it.
|
||||
this.settingsGroup = new ComfyButtonGroup();
|
||||
this.viewGroup = new ComfyButtonGroup(
|
||||
getInteruptButton("nlg-hide").element
|
||||
);
|
||||
this.settingsGroup = new ComfyButtonGroup()
|
||||
this.viewGroup = new ComfyButtonGroup(getInteruptButton('nlg-hide').element)
|
||||
this.mobileMenuButton = new ComfyButton({
|
||||
icon: "menu",
|
||||
icon: 'menu',
|
||||
action: (_, btn) => {
|
||||
btn.icon = this.element.classList.toggle("expanded")
|
||||
? "menu-open"
|
||||
: "menu";
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
btn.icon = this.element.classList.toggle('expanded')
|
||||
? 'menu-open'
|
||||
: 'menu'
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
},
|
||||
classList: "comfyui-button comfyui-menu-button",
|
||||
});
|
||||
classList: 'comfyui-button comfyui-menu-button'
|
||||
})
|
||||
|
||||
this.element = $el("nav.comfyui-menu.lg", { style: { display: "none" } }, [
|
||||
this.element = $el('nav.comfyui-menu.lg', { style: { display: 'none' } }, [
|
||||
this.logo,
|
||||
this.workflows.element,
|
||||
this.saveButton.element,
|
||||
collapseOnMobile(this.actionsGroup).element,
|
||||
$el("section.comfyui-menu-push"),
|
||||
$el('section.comfyui-menu-push'),
|
||||
collapseOnMobile(this.settingsGroup).element,
|
||||
collapseOnMobile(this.viewGroup).element,
|
||||
|
||||
getInteruptButton("lt-lg-show").element,
|
||||
getInteruptButton('lt-lg-show').element,
|
||||
new ComfyQueueButton(app).element,
|
||||
showOnMobile(this.mobileMenuButton).element,
|
||||
]);
|
||||
showOnMobile(this.mobileMenuButton).element
|
||||
])
|
||||
|
||||
let resizeHandler: () => void;
|
||||
let resizeHandler: () => void
|
||||
this.menuPositionSetting = app.ui.settings.addSetting({
|
||||
id: "Comfy.UseNewMenu",
|
||||
defaultValue: "Disabled",
|
||||
name: "[Beta] Use new menu and workflow management. Note: On small screens the menu will always be at the top.",
|
||||
type: "combo",
|
||||
options: ["Disabled", "Top", "Bottom"],
|
||||
id: 'Comfy.UseNewMenu',
|
||||
defaultValue: 'Disabled',
|
||||
name: '[Beta] Use new menu and workflow management. Note: On small screens the menu will always be at the top.',
|
||||
type: 'combo',
|
||||
options: ['Disabled', 'Top', 'Bottom'],
|
||||
onChange: async (v: MenuPosition) => {
|
||||
if (v && v !== "Disabled") {
|
||||
if (v && v !== 'Disabled') {
|
||||
if (!resizeHandler) {
|
||||
resizeHandler = () => {
|
||||
this.calculateSizeBreak();
|
||||
};
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
this.calculateSizeBreak()
|
||||
}
|
||||
this.updatePosition(v);
|
||||
window.addEventListener('resize', resizeHandler)
|
||||
}
|
||||
this.updatePosition(v)
|
||||
} else {
|
||||
if (resizeHandler) {
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
resizeHandler = null;
|
||||
window.removeEventListener('resize', resizeHandler)
|
||||
resizeHandler = null
|
||||
}
|
||||
document.body.style.removeProperty("display");
|
||||
app.ui.menuContainer.style.removeProperty("display");
|
||||
this.element.style.display = "none";
|
||||
app.ui.restoreMenuPosition();
|
||||
document.body.style.removeProperty('display')
|
||||
app.ui.menuContainer.style.removeProperty('display')
|
||||
this.element.style.display = 'none'
|
||||
app.ui.restoreMenuPosition()
|
||||
}
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
},
|
||||
});
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
updatePosition(v: MenuPosition) {
|
||||
document.body.style.display = "grid";
|
||||
this.app.ui.menuContainer.style.display = "none";
|
||||
this.element.style.removeProperty("display");
|
||||
this.position = v;
|
||||
if (v === "Bottom") {
|
||||
this.app.bodyBottom.append(this.element);
|
||||
document.body.style.display = 'grid'
|
||||
this.app.ui.menuContainer.style.display = 'none'
|
||||
this.element.style.removeProperty('display')
|
||||
this.position = v
|
||||
if (v === 'Bottom') {
|
||||
this.app.bodyBottom.append(this.element)
|
||||
} else {
|
||||
this.app.bodyTop.prepend(this.element);
|
||||
this.app.bodyTop.prepend(this.element)
|
||||
}
|
||||
this.calculateSizeBreak();
|
||||
this.calculateSizeBreak()
|
||||
}
|
||||
|
||||
updateSizeBreak(idx: number, prevIdx: number, direction: number) {
|
||||
const newSize = this.#sizeBreaks[idx];
|
||||
if (newSize === this.#sizeBreak) return;
|
||||
this.#cachedInnerSize = null;
|
||||
clearTimeout(this.#cacheTimeout);
|
||||
const newSize = this.#sizeBreaks[idx]
|
||||
if (newSize === this.#sizeBreak) return
|
||||
this.#cachedInnerSize = null
|
||||
clearTimeout(this.#cacheTimeout)
|
||||
|
||||
this.#sizeBreak = this.#sizeBreaks[idx];
|
||||
this.#sizeBreak = this.#sizeBreaks[idx]
|
||||
for (let i = 0; i < this.#sizeBreaks.length; i++) {
|
||||
const sz = this.#sizeBreaks[i];
|
||||
const sz = this.#sizeBreaks[i]
|
||||
if (sz === this.#sizeBreak) {
|
||||
this.element.classList.add(sz);
|
||||
this.element.classList.add(sz)
|
||||
} else {
|
||||
this.element.classList.remove(sz);
|
||||
this.element.classList.remove(sz)
|
||||
}
|
||||
if (i < idx) {
|
||||
this.element.classList.add("lt-" + sz);
|
||||
this.element.classList.add('lt-' + sz)
|
||||
} else {
|
||||
this.element.classList.remove("lt-" + sz);
|
||||
this.element.classList.remove('lt-' + sz)
|
||||
}
|
||||
}
|
||||
|
||||
if (idx) {
|
||||
// We're on a small screen, force the menu at the top
|
||||
if (this.position !== "Top") {
|
||||
this.updatePosition("Top");
|
||||
if (this.position !== 'Top') {
|
||||
this.updatePosition('Top')
|
||||
}
|
||||
} else if (this.position != this.menuPositionSetting.value) {
|
||||
// Restore user position
|
||||
this.updatePosition(this.menuPositionSetting.value);
|
||||
this.updatePosition(this.menuPositionSetting.value)
|
||||
}
|
||||
|
||||
// Allow multiple updates, but prevent bouncing
|
||||
if (!direction) {
|
||||
direction = prevIdx - idx;
|
||||
direction = prevIdx - idx
|
||||
} else if (direction != prevIdx - idx) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
this.calculateSizeBreak(direction);
|
||||
this.calculateSizeBreak(direction)
|
||||
}
|
||||
|
||||
calculateSizeBreak(direction = 0) {
|
||||
let idx = this.#sizeBreaks.indexOf(this.#sizeBreak);
|
||||
const currIdx = idx;
|
||||
const innerSize = this.calculateInnerSize(idx);
|
||||
let idx = this.#sizeBreaks.indexOf(this.#sizeBreak)
|
||||
const currIdx = idx
|
||||
const innerSize = this.calculateInnerSize(idx)
|
||||
if (window.innerWidth >= this.#lastSizeBreaks[this.#sizeBreaks[idx - 1]]) {
|
||||
if (idx > 0) {
|
||||
idx--;
|
||||
idx--
|
||||
}
|
||||
} else if (innerSize > this.element.clientWidth) {
|
||||
this.#lastSizeBreaks[this.#sizeBreak] = Math.max(
|
||||
window.innerWidth,
|
||||
innerSize
|
||||
);
|
||||
)
|
||||
// We need to shrink
|
||||
if (idx < this.#sizeBreaks.length - 1) {
|
||||
idx++;
|
||||
idx++
|
||||
}
|
||||
}
|
||||
|
||||
this.updateSizeBreak(idx, currIdx, direction);
|
||||
this.updateSizeBreak(idx, currIdx, direction)
|
||||
}
|
||||
|
||||
calculateInnerSize(idx: number) {
|
||||
// Cache the inner size to prevent too much calculation when resizing the window
|
||||
clearTimeout(this.#cacheTimeout);
|
||||
clearTimeout(this.#cacheTimeout)
|
||||
if (this.#cachedInnerSize) {
|
||||
// Extend cache time
|
||||
this.#cacheTimeout = setTimeout(
|
||||
() => (this.#cachedInnerSize = null),
|
||||
100
|
||||
);
|
||||
this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100)
|
||||
} else {
|
||||
let innerSize = 0;
|
||||
let count = 1;
|
||||
let innerSize = 0
|
||||
let count = 1
|
||||
for (const c of this.element.children) {
|
||||
if (c.classList.contains("comfyui-menu-push")) continue; // ignore right push
|
||||
if (idx && c.classList.contains("comfyui-menu-mobile-collapse"))
|
||||
continue; // ignore collapse items
|
||||
innerSize += c.clientWidth;
|
||||
count++;
|
||||
if (c.classList.contains('comfyui-menu-push')) continue // ignore right push
|
||||
if (idx && c.classList.contains('comfyui-menu-mobile-collapse'))
|
||||
continue // ignore collapse items
|
||||
innerSize += c.clientWidth
|
||||
count++
|
||||
}
|
||||
innerSize += 8 * count;
|
||||
this.#cachedInnerSize = innerSize;
|
||||
this.#cacheTimeout = setTimeout(
|
||||
() => (this.#cachedInnerSize = null),
|
||||
100
|
||||
);
|
||||
innerSize += 8 * count
|
||||
this.#cachedInnerSize = innerSize
|
||||
this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100)
|
||||
}
|
||||
return this.#cachedInnerSize;
|
||||
return this.#cachedInnerSize
|
||||
}
|
||||
|
||||
getFilename(defaultName: string) {
|
||||
if (this.app.ui.settings.getSettingValue("Comfy.PromptFilename", true)) {
|
||||
defaultName = prompt("Save workflow as:", defaultName);
|
||||
if (!defaultName) return;
|
||||
if (!defaultName.toLowerCase().endsWith(".json")) {
|
||||
defaultName += ".json";
|
||||
if (this.app.ui.settings.getSettingValue('Comfy.PromptFilename', true)) {
|
||||
defaultName = prompt('Save workflow as:', defaultName)
|
||||
if (!defaultName) return
|
||||
if (!defaultName.toLowerCase().endsWith('.json')) {
|
||||
defaultName += '.json'
|
||||
}
|
||||
}
|
||||
return defaultName;
|
||||
return defaultName
|
||||
}
|
||||
|
||||
async exportWorkflow(
|
||||
filename: string,
|
||||
promptProperty: "workflow" | "output"
|
||||
promptProperty: 'workflow' | 'output'
|
||||
) {
|
||||
if (this.app.workflowManager.activeWorkflow?.path) {
|
||||
filename = this.app.workflowManager.activeWorkflow.name;
|
||||
filename = this.app.workflowManager.activeWorkflow.name
|
||||
}
|
||||
const p = await this.app.graphToPrompt();
|
||||
const json = JSON.stringify(p[promptProperty], null, 2);
|
||||
const blob = new Blob([json], { type: "application/json" });
|
||||
const file = this.getFilename(filename);
|
||||
if (!file) return;
|
||||
downloadBlob(file, blob);
|
||||
const p = await this.app.graphToPrompt()
|
||||
const json = JSON.stringify(p[promptProperty], null, 2)
|
||||
const blob = new Blob([json], { type: 'application/json' })
|
||||
const file = this.getFilename(filename)
|
||||
if (!file) return
|
||||
downloadBlob(file, blob)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { api } from "../../api";
|
||||
import { ComfyButton } from "../components/button";
|
||||
import { api } from '../../api'
|
||||
import { ComfyButton } from '../components/button'
|
||||
|
||||
export function getInteruptButton(visibility: string) {
|
||||
const btn = new ComfyButton({
|
||||
icon: "close",
|
||||
tooltip: "Cancel current generation",
|
||||
icon: 'close',
|
||||
tooltip: 'Cancel current generation',
|
||||
enabled: false,
|
||||
action: () => {
|
||||
api.interrupt();
|
||||
api.interrupt()
|
||||
},
|
||||
classList: ["comfyui-button", "comfyui-interrupt-button", visibility],
|
||||
});
|
||||
classList: ['comfyui-button', 'comfyui-interrupt-button', visibility]
|
||||
})
|
||||
|
||||
api.addEventListener("status", ({ detail }) => {
|
||||
const sz = detail?.exec_info?.queue_remaining;
|
||||
btn.enabled = sz > 0;
|
||||
});
|
||||
api.addEventListener('status', ({ detail }) => {
|
||||
const sz = detail?.exec_info?.queue_remaining
|
||||
btn.enabled = sz > 0
|
||||
})
|
||||
|
||||
return btn;
|
||||
return btn
|
||||
}
|
||||
|
||||
@@ -1,107 +1,107 @@
|
||||
import { ComfyButton } from "../components/button";
|
||||
import { $el } from "../../ui";
|
||||
import { api } from "../../api";
|
||||
import { ComfySplitButton } from "../components/splitButton";
|
||||
import { ComfyQueueOptions } from "./queueOptions";
|
||||
import { prop } from "../../utils";
|
||||
import type { ComfyApp } from "@/scripts/app";
|
||||
import { ComfyButton } from '../components/button'
|
||||
import { $el } from '../../ui'
|
||||
import { api } from '../../api'
|
||||
import { ComfySplitButton } from '../components/splitButton'
|
||||
import { ComfyQueueOptions } from './queueOptions'
|
||||
import { prop } from '../../utils'
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
|
||||
export class ComfyQueueButton {
|
||||
element = $el("div.comfyui-queue-button");
|
||||
#internalQueueSize = 0;
|
||||
element = $el('div.comfyui-queue-button')
|
||||
#internalQueueSize = 0
|
||||
|
||||
queuePrompt = async (e?: MouseEvent) => {
|
||||
this.#internalQueueSize += this.queueOptions.batchCount;
|
||||
this.#internalQueueSize += this.queueOptions.batchCount
|
||||
// Hold shift to queue front, event is undefined when auto-queue is enabled
|
||||
await this.app.queuePrompt(
|
||||
e?.shiftKey ? -1 : 0,
|
||||
this.queueOptions.batchCount
|
||||
);
|
||||
};
|
||||
queueOptions: ComfyQueueOptions;
|
||||
app: ComfyApp;
|
||||
queueSizeElement: HTMLElement;
|
||||
autoQueueMode: string;
|
||||
graphHasChanged: boolean;
|
||||
)
|
||||
}
|
||||
queueOptions: ComfyQueueOptions
|
||||
app: ComfyApp
|
||||
queueSizeElement: HTMLElement
|
||||
autoQueueMode: string
|
||||
graphHasChanged: boolean
|
||||
|
||||
constructor(app: ComfyApp) {
|
||||
this.app = app;
|
||||
this.queueSizeElement = $el("span.comfyui-queue-count", {
|
||||
textContent: "?",
|
||||
});
|
||||
this.app = app
|
||||
this.queueSizeElement = $el('span.comfyui-queue-count', {
|
||||
textContent: '?'
|
||||
})
|
||||
|
||||
const queue = new ComfyButton({
|
||||
content: $el("div", [
|
||||
$el("span", {
|
||||
textContent: "Queue",
|
||||
content: $el('div', [
|
||||
$el('span', {
|
||||
textContent: 'Queue'
|
||||
}),
|
||||
this.queueSizeElement,
|
||||
this.queueSizeElement
|
||||
]),
|
||||
icon: "play",
|
||||
classList: "comfyui-button",
|
||||
action: this.queuePrompt,
|
||||
});
|
||||
icon: 'play',
|
||||
classList: 'comfyui-button',
|
||||
action: this.queuePrompt
|
||||
})
|
||||
|
||||
this.queueOptions = new ComfyQueueOptions(app);
|
||||
this.queueOptions = new ComfyQueueOptions(app)
|
||||
|
||||
const btn = new ComfySplitButton(
|
||||
{
|
||||
primary: queue,
|
||||
mode: "click",
|
||||
position: "absolute",
|
||||
horizontal: "right",
|
||||
mode: 'click',
|
||||
position: 'absolute',
|
||||
horizontal: 'right'
|
||||
},
|
||||
this.queueOptions.element
|
||||
);
|
||||
btn.element.classList.add("primary");
|
||||
this.element.append(btn.element);
|
||||
)
|
||||
btn.element.classList.add('primary')
|
||||
this.element.append(btn.element)
|
||||
|
||||
this.autoQueueMode = prop(this, "autoQueueMode", "", () => {
|
||||
this.autoQueueMode = prop(this, 'autoQueueMode', '', () => {
|
||||
switch (this.autoQueueMode) {
|
||||
case "instant":
|
||||
queue.icon = "infinity";
|
||||
break;
|
||||
case "change":
|
||||
queue.icon = "auto-mode";
|
||||
break;
|
||||
case 'instant':
|
||||
queue.icon = 'infinity'
|
||||
break
|
||||
case 'change':
|
||||
queue.icon = 'auto-mode'
|
||||
break
|
||||
default:
|
||||
queue.icon = "play";
|
||||
break;
|
||||
queue.icon = 'play'
|
||||
break
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
this.queueOptions.addEventListener(
|
||||
"autoQueueMode",
|
||||
(e) => (this.autoQueueMode = e["detail"])
|
||||
);
|
||||
'autoQueueMode',
|
||||
(e) => (this.autoQueueMode = e['detail'])
|
||||
)
|
||||
|
||||
api.addEventListener("graphChanged", () => {
|
||||
if (this.autoQueueMode === "change") {
|
||||
api.addEventListener('graphChanged', () => {
|
||||
if (this.autoQueueMode === 'change') {
|
||||
if (this.#internalQueueSize) {
|
||||
this.graphHasChanged = true;
|
||||
this.graphHasChanged = true
|
||||
} else {
|
||||
this.graphHasChanged = false;
|
||||
this.queuePrompt();
|
||||
this.graphHasChanged = false
|
||||
this.queuePrompt()
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
api.addEventListener("status", ({ detail }) => {
|
||||
this.#internalQueueSize = detail?.exec_info?.queue_remaining;
|
||||
api.addEventListener('status', ({ detail }) => {
|
||||
this.#internalQueueSize = detail?.exec_info?.queue_remaining
|
||||
if (this.#internalQueueSize != null) {
|
||||
this.queueSizeElement.textContent =
|
||||
this.#internalQueueSize > 99 ? "99+" : this.#internalQueueSize + "";
|
||||
this.queueSizeElement.title = `${this.#internalQueueSize} prompts in queue`;
|
||||
this.#internalQueueSize > 99 ? '99+' : this.#internalQueueSize + ''
|
||||
this.queueSizeElement.title = `${this.#internalQueueSize} prompts in queue`
|
||||
if (!this.#internalQueueSize && !app.lastExecutionError) {
|
||||
if (
|
||||
this.autoQueueMode === "instant" ||
|
||||
(this.autoQueueMode === "change" && this.graphHasChanged)
|
||||
this.autoQueueMode === 'instant' ||
|
||||
(this.autoQueueMode === 'change' && this.graphHasChanged)
|
||||
) {
|
||||
this.graphHasChanged = false;
|
||||
this.queuePrompt();
|
||||
this.graphHasChanged = false
|
||||
this.queuePrompt()
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,82 +1,82 @@
|
||||
import type { ComfyApp } from "@/scripts/app";
|
||||
import { $el } from "../../ui";
|
||||
import { prop } from "../../utils";
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
import { $el } from '../../ui'
|
||||
import { prop } from '../../utils'
|
||||
|
||||
export class ComfyQueueOptions extends EventTarget {
|
||||
element = $el("div.comfyui-queue-options");
|
||||
app: ComfyApp;
|
||||
batchCountInput: HTMLInputElement;
|
||||
batchCount: number;
|
||||
batchCountRange: HTMLInputElement;
|
||||
autoQueueMode: string;
|
||||
autoQueueEl: HTMLElement;
|
||||
element = $el('div.comfyui-queue-options')
|
||||
app: ComfyApp
|
||||
batchCountInput: HTMLInputElement
|
||||
batchCount: number
|
||||
batchCountRange: HTMLInputElement
|
||||
autoQueueMode: string
|
||||
autoQueueEl: HTMLElement
|
||||
|
||||
constructor(app: ComfyApp) {
|
||||
super();
|
||||
this.app = app;
|
||||
super()
|
||||
this.app = app
|
||||
|
||||
this.batchCountInput = $el("input", {
|
||||
className: "comfyui-queue-batch-value",
|
||||
type: "number",
|
||||
min: "1",
|
||||
value: "1",
|
||||
oninput: () => (this.batchCount = +this.batchCountInput.value),
|
||||
});
|
||||
this.batchCountInput = $el('input', {
|
||||
className: 'comfyui-queue-batch-value',
|
||||
type: 'number',
|
||||
min: '1',
|
||||
value: '1',
|
||||
oninput: () => (this.batchCount = +this.batchCountInput.value)
|
||||
})
|
||||
|
||||
this.batchCountRange = $el("input", {
|
||||
type: "range",
|
||||
min: "1",
|
||||
max: "100",
|
||||
value: "1",
|
||||
oninput: () => (this.batchCount = +this.batchCountRange.value),
|
||||
});
|
||||
this.batchCountRange = $el('input', {
|
||||
type: 'range',
|
||||
min: '1',
|
||||
max: '100',
|
||||
value: '1',
|
||||
oninput: () => (this.batchCount = +this.batchCountRange.value)
|
||||
})
|
||||
|
||||
this.element.append(
|
||||
$el("div.comfyui-queue-batch", [
|
||||
$el('div.comfyui-queue-batch', [
|
||||
$el(
|
||||
"label",
|
||||
'label',
|
||||
{
|
||||
textContent: "Batch count: ",
|
||||
textContent: 'Batch count: '
|
||||
},
|
||||
this.batchCountInput
|
||||
),
|
||||
this.batchCountRange,
|
||||
this.batchCountRange
|
||||
])
|
||||
);
|
||||
)
|
||||
|
||||
const createOption = (text, value, checked = false) =>
|
||||
$el(
|
||||
"label",
|
||||
'label',
|
||||
{ textContent: text },
|
||||
$el("input", {
|
||||
type: "radio",
|
||||
name: "AutoQueueMode",
|
||||
$el('input', {
|
||||
type: 'radio',
|
||||
name: 'AutoQueueMode',
|
||||
checked,
|
||||
value,
|
||||
oninput: (e) => (this.autoQueueMode = e.target["value"]),
|
||||
oninput: (e) => (this.autoQueueMode = e.target['value'])
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
this.autoQueueEl = $el("div.comfyui-queue-mode", [
|
||||
$el("span", "Auto Queue:"),
|
||||
createOption("Disabled", "", true),
|
||||
createOption("Instant", "instant"),
|
||||
createOption("On Change", "change"),
|
||||
]);
|
||||
this.autoQueueEl = $el('div.comfyui-queue-mode', [
|
||||
$el('span', 'Auto Queue:'),
|
||||
createOption('Disabled', '', true),
|
||||
createOption('Instant', 'instant'),
|
||||
createOption('On Change', 'change')
|
||||
])
|
||||
|
||||
this.element.append(this.autoQueueEl);
|
||||
this.element.append(this.autoQueueEl)
|
||||
|
||||
this.batchCount = prop(this, "batchCount", 1, () => {
|
||||
this.batchCountInput.value = this.batchCount + "";
|
||||
this.batchCountRange.value = this.batchCount + "";
|
||||
});
|
||||
this.batchCount = prop(this, 'batchCount', 1, () => {
|
||||
this.batchCountInput.value = this.batchCount + ''
|
||||
this.batchCountRange.value = this.batchCount + ''
|
||||
})
|
||||
|
||||
this.autoQueueMode = prop(this, "autoQueueMode", "Disabled", () => {
|
||||
this.autoQueueMode = prop(this, 'autoQueueMode', 'Disabled', () => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("autoQueueMode", {
|
||||
detail: this.autoQueueMode,
|
||||
new CustomEvent('autoQueueMode', {
|
||||
detail: this.autoQueueMode
|
||||
})
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,60 +1,60 @@
|
||||
import { $el } from "../ui";
|
||||
import { api } from "../api";
|
||||
import { ComfyDialog } from "./dialog";
|
||||
import type { ComfyApp } from "../app";
|
||||
import type { Setting, SettingParams } from "@/types/settingTypes";
|
||||
import { useSettingStore } from "@/stores/settingStore";
|
||||
import { $el } from '../ui'
|
||||
import { api } from '../api'
|
||||
import { ComfyDialog } from './dialog'
|
||||
import type { ComfyApp } from '../app'
|
||||
import type { Setting, SettingParams } from '@/types/settingTypes'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
app: ComfyApp;
|
||||
settingsValues: any;
|
||||
settingsLookup: Record<string, Setting>;
|
||||
settingsParamLookup: Record<string, SettingParams>;
|
||||
app: ComfyApp
|
||||
settingsValues: any
|
||||
settingsLookup: Record<string, Setting>
|
||||
settingsParamLookup: Record<string, SettingParams>
|
||||
|
||||
constructor(app: ComfyApp) {
|
||||
super();
|
||||
const frontendVersion = window["__COMFYUI_FRONTEND_VERSION__"];
|
||||
this.app = app;
|
||||
this.settingsValues = {};
|
||||
this.settingsLookup = {};
|
||||
this.settingsParamLookup = {};
|
||||
super()
|
||||
const frontendVersion = window['__COMFYUI_FRONTEND_VERSION__']
|
||||
this.app = app
|
||||
this.settingsValues = {}
|
||||
this.settingsLookup = {}
|
||||
this.settingsParamLookup = {}
|
||||
this.element = $el(
|
||||
"dialog",
|
||||
'dialog',
|
||||
{
|
||||
id: "comfy-settings-dialog",
|
||||
parent: document.body,
|
||||
id: 'comfy-settings-dialog',
|
||||
parent: document.body
|
||||
},
|
||||
[
|
||||
$el("table.comfy-modal-content.comfy-table", [
|
||||
$el('table.comfy-modal-content.comfy-table', [
|
||||
$el(
|
||||
"caption",
|
||||
'caption',
|
||||
{ textContent: `Settings (v${frontendVersion})` },
|
||||
$el("button.comfy-btn", {
|
||||
type: "button",
|
||||
textContent: "\u00d7",
|
||||
$el('button.comfy-btn', {
|
||||
type: 'button',
|
||||
textContent: '\u00d7',
|
||||
onclick: () => {
|
||||
this.element.close();
|
||||
},
|
||||
this.element.close()
|
||||
}
|
||||
})
|
||||
),
|
||||
$el("tbody", { $: (tbody) => (this.textElement = tbody) }),
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Close",
|
||||
$el('tbody', { $: (tbody) => (this.textElement = tbody) }),
|
||||
$el('button', {
|
||||
type: 'button',
|
||||
textContent: 'Close',
|
||||
style: {
|
||||
cursor: "pointer",
|
||||
cursor: 'pointer'
|
||||
},
|
||||
onclick: () => {
|
||||
this.element.close();
|
||||
},
|
||||
}),
|
||||
]),
|
||||
this.element.close()
|
||||
}
|
||||
})
|
||||
])
|
||||
]
|
||||
) as HTMLDialogElement;
|
||||
) as HTMLDialogElement
|
||||
}
|
||||
|
||||
get settings() {
|
||||
return Object.values(this.settingsLookup);
|
||||
return Object.values(this.settingsLookup)
|
||||
}
|
||||
|
||||
#dispatchChange<T>(id: string, value: T, oldValue?: T) {
|
||||
@@ -63,84 +63,84 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
// `load` re-dispatch the change for any settings added before load so
|
||||
// settingStore is always up to date.
|
||||
if (this.app.vueAppReady) {
|
||||
useSettingStore().settingValues[id] = value;
|
||||
useSettingStore().settingValues[id] = value
|
||||
}
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(id + ".change", {
|
||||
new CustomEvent(id + '.change', {
|
||||
detail: {
|
||||
value,
|
||||
oldValue,
|
||||
},
|
||||
oldValue
|
||||
}
|
||||
})
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (this.app.storageLocation === "browser") {
|
||||
this.settingsValues = localStorage;
|
||||
if (this.app.storageLocation === 'browser') {
|
||||
this.settingsValues = localStorage
|
||||
} else {
|
||||
this.settingsValues = await api.getSettings();
|
||||
this.settingsValues = await api.getSettings()
|
||||
}
|
||||
|
||||
// Trigger onChange for any settings added before load
|
||||
for (const id in this.settingsLookup) {
|
||||
const value = this.settingsValues[this.getId(id)];
|
||||
this.settingsLookup[id].onChange?.(value);
|
||||
this.#dispatchChange(id, value);
|
||||
const value = this.settingsValues[this.getId(id)]
|
||||
this.settingsLookup[id].onChange?.(value)
|
||||
this.#dispatchChange(id, value)
|
||||
}
|
||||
}
|
||||
|
||||
getId(id: string) {
|
||||
if (this.app.storageLocation === "browser") {
|
||||
id = "Comfy.Settings." + id;
|
||||
if (this.app.storageLocation === 'browser') {
|
||||
id = 'Comfy.Settings.' + id
|
||||
}
|
||||
return id;
|
||||
return id
|
||||
}
|
||||
|
||||
getSettingValue<T>(id: string, defaultValue?: T): T {
|
||||
let value = this.settingsValues[this.getId(id)];
|
||||
let value = this.settingsValues[this.getId(id)]
|
||||
if (value != null) {
|
||||
if (this.app.storageLocation === "browser") {
|
||||
if (this.app.storageLocation === 'browser') {
|
||||
try {
|
||||
value = JSON.parse(value) as T;
|
||||
value = JSON.parse(value) as T
|
||||
} catch (error) {}
|
||||
}
|
||||
}
|
||||
return value ?? defaultValue;
|
||||
return value ?? defaultValue
|
||||
}
|
||||
|
||||
getSettingDefaultValue(id: string) {
|
||||
const param = this.settingsParamLookup[id];
|
||||
return param?.defaultValue;
|
||||
const param = this.settingsParamLookup[id]
|
||||
return param?.defaultValue
|
||||
}
|
||||
|
||||
async setSettingValueAsync(id: string, value: any) {
|
||||
const json = JSON.stringify(value);
|
||||
localStorage["Comfy.Settings." + id] = json; // backwards compatibility for extensions keep setting in storage
|
||||
const json = JSON.stringify(value)
|
||||
localStorage['Comfy.Settings.' + id] = json // backwards compatibility for extensions keep setting in storage
|
||||
|
||||
let oldValue = this.getSettingValue(id, undefined);
|
||||
this.settingsValues[this.getId(id)] = value;
|
||||
let oldValue = this.getSettingValue(id, undefined)
|
||||
this.settingsValues[this.getId(id)] = value
|
||||
|
||||
if (id in this.settingsLookup) {
|
||||
this.settingsLookup[id].onChange?.(value, oldValue);
|
||||
this.settingsLookup[id].onChange?.(value, oldValue)
|
||||
}
|
||||
this.#dispatchChange(id, value, oldValue);
|
||||
this.#dispatchChange(id, value, oldValue)
|
||||
|
||||
await api.storeSetting(id, value);
|
||||
await api.storeSetting(id, value)
|
||||
}
|
||||
|
||||
setSettingValue(id: string, value: any) {
|
||||
this.setSettingValueAsync(id, value).catch((err) => {
|
||||
alert(`Error saving setting '${id}'`);
|
||||
console.error(err);
|
||||
});
|
||||
alert(`Error saving setting '${id}'`)
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
refreshSetting(id: string) {
|
||||
const value = this.getSettingValue(id);
|
||||
this.settingsLookup[id].onChange?.(value);
|
||||
this.#dispatchChange(id, value);
|
||||
const value = this.getSettingValue(id)
|
||||
this.settingsLookup[id].onChange?.(value)
|
||||
this.#dispatchChange(id, value)
|
||||
}
|
||||
|
||||
addSetting(params: SettingParams) {
|
||||
@@ -151,232 +151,231 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
defaultValue,
|
||||
onChange,
|
||||
attrs = {},
|
||||
tooltip = "",
|
||||
options = undefined,
|
||||
} = params;
|
||||
tooltip = '',
|
||||
options = undefined
|
||||
} = params
|
||||
if (!id) {
|
||||
throw new Error("Settings must have an ID");
|
||||
throw new Error('Settings must have an ID')
|
||||
}
|
||||
|
||||
if (id in this.settingsLookup) {
|
||||
throw new Error(`Setting ${id} of type ${type} must have a unique ID.`);
|
||||
throw new Error(`Setting ${id} of type ${type} must have a unique ID.`)
|
||||
}
|
||||
|
||||
let skipOnChange = false;
|
||||
let value = this.getSettingValue(id);
|
||||
let skipOnChange = false
|
||||
let value = this.getSettingValue(id)
|
||||
if (value == null) {
|
||||
if (this.app.isNewUserSession) {
|
||||
// Check if we have a localStorage value but not a setting value and we are a new user
|
||||
const localValue = localStorage["Comfy.Settings." + id];
|
||||
const localValue = localStorage['Comfy.Settings.' + id]
|
||||
if (localValue) {
|
||||
value = JSON.parse(localValue);
|
||||
this.setSettingValue(id, value); // Store on the server
|
||||
value = JSON.parse(localValue)
|
||||
this.setSettingValue(id, value) // Store on the server
|
||||
}
|
||||
}
|
||||
if (value == null) {
|
||||
value = defaultValue;
|
||||
value = defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger initial setting of value
|
||||
if (!skipOnChange) {
|
||||
onChange?.(value, undefined);
|
||||
this.#dispatchChange(id, value);
|
||||
onChange?.(value, undefined)
|
||||
this.#dispatchChange(id, value)
|
||||
}
|
||||
|
||||
this.settingsParamLookup[id] = params;
|
||||
this.settingsParamLookup[id] = params
|
||||
this.settingsLookup[id] = {
|
||||
id,
|
||||
onChange,
|
||||
name,
|
||||
render: () => {
|
||||
if (type === "hidden") return;
|
||||
if (type === 'hidden') return
|
||||
|
||||
const setter = (v) => {
|
||||
if (onChange) {
|
||||
onChange(v, value);
|
||||
onChange(v, value)
|
||||
}
|
||||
|
||||
this.setSettingValue(id, v);
|
||||
value = v;
|
||||
};
|
||||
value = this.getSettingValue(id, defaultValue);
|
||||
this.setSettingValue(id, v)
|
||||
value = v
|
||||
}
|
||||
value = this.getSettingValue(id, defaultValue)
|
||||
|
||||
let element;
|
||||
const htmlID = id.replaceAll(".", "-");
|
||||
let element
|
||||
const htmlID = id.replaceAll('.', '-')
|
||||
|
||||
const labelCell = $el("td", [
|
||||
$el("label", {
|
||||
const labelCell = $el('td', [
|
||||
$el('label', {
|
||||
for: htmlID,
|
||||
classList: [tooltip !== "" ? "comfy-tooltip-indicator" : ""],
|
||||
textContent: name,
|
||||
}),
|
||||
]);
|
||||
classList: [tooltip !== '' ? 'comfy-tooltip-indicator' : ''],
|
||||
textContent: name
|
||||
})
|
||||
])
|
||||
|
||||
if (typeof type === "function") {
|
||||
element = type(name, setter, value, attrs);
|
||||
if (typeof type === 'function') {
|
||||
element = type(name, setter, value, attrs)
|
||||
} else {
|
||||
switch (type) {
|
||||
case "boolean":
|
||||
element = $el("tr", [
|
||||
case 'boolean':
|
||||
element = $el('tr', [
|
||||
labelCell,
|
||||
$el("td", [
|
||||
$el("input", {
|
||||
$el('td', [
|
||||
$el('input', {
|
||||
id: htmlID,
|
||||
type: "checkbox",
|
||||
type: 'checkbox',
|
||||
checked: value,
|
||||
onchange: (event) => {
|
||||
const isChecked = event.target.checked;
|
||||
const isChecked = event.target.checked
|
||||
if (onChange !== undefined) {
|
||||
onChange(isChecked);
|
||||
onChange(isChecked)
|
||||
}
|
||||
this.setSettingValue(id, isChecked);
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
case "number":
|
||||
element = $el("tr", [
|
||||
this.setSettingValue(id, isChecked)
|
||||
}
|
||||
})
|
||||
])
|
||||
])
|
||||
break
|
||||
case 'number':
|
||||
element = $el('tr', [
|
||||
labelCell,
|
||||
$el("td", [
|
||||
$el("input", {
|
||||
$el('td', [
|
||||
$el('input', {
|
||||
type,
|
||||
value,
|
||||
id: htmlID,
|
||||
oninput: (e) => {
|
||||
setter(e.target.value);
|
||||
setter(e.target.value)
|
||||
},
|
||||
...attrs,
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
case "slider":
|
||||
element = $el("tr", [
|
||||
...attrs
|
||||
})
|
||||
])
|
||||
])
|
||||
break
|
||||
case 'slider':
|
||||
element = $el('tr', [
|
||||
labelCell,
|
||||
$el("td", [
|
||||
$el('td', [
|
||||
$el(
|
||||
"div",
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
display: "grid",
|
||||
gridAutoFlow: "column",
|
||||
},
|
||||
display: 'grid',
|
||||
gridAutoFlow: 'column'
|
||||
}
|
||||
},
|
||||
[
|
||||
$el("input", {
|
||||
$el('input', {
|
||||
...attrs,
|
||||
value,
|
||||
type: "range",
|
||||
type: 'range',
|
||||
oninput: (e) => {
|
||||
setter(e.target.value);
|
||||
e.target.nextElementSibling.value = e.target.value;
|
||||
},
|
||||
setter(e.target.value)
|
||||
e.target.nextElementSibling.value = e.target.value
|
||||
}
|
||||
}),
|
||||
$el("input", {
|
||||
$el('input', {
|
||||
...attrs,
|
||||
value,
|
||||
id: htmlID,
|
||||
type: "number",
|
||||
style: { maxWidth: "4rem" },
|
||||
type: 'number',
|
||||
style: { maxWidth: '4rem' },
|
||||
oninput: (e) => {
|
||||
setter(e.target.value);
|
||||
e.target.previousElementSibling.value =
|
||||
e.target.value;
|
||||
},
|
||||
}),
|
||||
setter(e.target.value)
|
||||
e.target.previousElementSibling.value = e.target.value
|
||||
}
|
||||
})
|
||||
]
|
||||
),
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
case "combo":
|
||||
element = $el("tr", [
|
||||
)
|
||||
])
|
||||
])
|
||||
break
|
||||
case 'combo':
|
||||
element = $el('tr', [
|
||||
labelCell,
|
||||
$el("td", [
|
||||
$el('td', [
|
||||
$el(
|
||||
"select",
|
||||
'select',
|
||||
{
|
||||
oninput: (e) => {
|
||||
setter(e.target.value);
|
||||
setter(e.target.value)
|
||||
}
|
||||
},
|
||||
},
|
||||
(typeof options === "function"
|
||||
(typeof options === 'function'
|
||||
? options(value)
|
||||
: options || []
|
||||
).map((opt) => {
|
||||
if (typeof opt === "string") {
|
||||
opt = { text: opt };
|
||||
if (typeof opt === 'string') {
|
||||
opt = { text: opt }
|
||||
}
|
||||
const v = opt.value ?? opt.text;
|
||||
return $el("option", {
|
||||
const v = opt.value ?? opt.text
|
||||
return $el('option', {
|
||||
value: v,
|
||||
textContent: opt.text,
|
||||
selected: value + "" === v + "",
|
||||
});
|
||||
selected: value + '' === v + ''
|
||||
})
|
||||
),
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
case "text":
|
||||
})
|
||||
)
|
||||
])
|
||||
])
|
||||
break
|
||||
case 'text':
|
||||
default:
|
||||
if (type !== "text") {
|
||||
if (type !== 'text') {
|
||||
console.warn(
|
||||
`Unsupported setting type '${type}, defaulting to text`
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
element = $el("tr", [
|
||||
element = $el('tr', [
|
||||
labelCell,
|
||||
$el("td", [
|
||||
$el("input", {
|
||||
$el('td', [
|
||||
$el('input', {
|
||||
value,
|
||||
id: htmlID,
|
||||
oninput: (e) => {
|
||||
setter(e.target.value);
|
||||
setter(e.target.value)
|
||||
},
|
||||
...attrs,
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
...attrs
|
||||
})
|
||||
])
|
||||
])
|
||||
break
|
||||
}
|
||||
}
|
||||
if (tooltip) {
|
||||
element.title = tooltip;
|
||||
element.title = tooltip
|
||||
}
|
||||
|
||||
return element;
|
||||
},
|
||||
} as Setting;
|
||||
return element
|
||||
}
|
||||
} as Setting
|
||||
|
||||
const self = this;
|
||||
const self = this
|
||||
return {
|
||||
get value() {
|
||||
return self.getSettingValue(id, defaultValue);
|
||||
return self.getSettingValue(id, defaultValue)
|
||||
},
|
||||
set value(v) {
|
||||
self.setSettingValue(id, v);
|
||||
},
|
||||
};
|
||||
self.setSettingValue(id, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
show() {
|
||||
this.textElement.replaceChildren(
|
||||
$el(
|
||||
"tr",
|
||||
'tr',
|
||||
{
|
||||
style: { display: "none" },
|
||||
style: { display: 'none' }
|
||||
},
|
||||
[$el("th"), $el("th", { style: { width: "33%" } })]
|
||||
[$el('th'), $el('th', { style: { width: '33%' } })]
|
||||
),
|
||||
...this.settings
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((s) => s.render())
|
||||
.filter(Boolean)
|
||||
);
|
||||
this.element.showModal();
|
||||
)
|
||||
this.element.showModal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "./spinner.css";
|
||||
import './spinner.css'
|
||||
|
||||
export function createSpinner() {
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = `<div class="lds-ring"><div></div><div></div><div></div><div></div></div>`;
|
||||
return div.firstElementChild;
|
||||
const div = document.createElement('div')
|
||||
div.innerHTML = `<div class="lds-ring"><div></div><div></div><div></div><div></div></div>`
|
||||
return div.firstElementChild
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { $el } from "../ui";
|
||||
import { $el } from '../ui'
|
||||
|
||||
/**
|
||||
* @typedef { { text: string, value?: string, tooltip?: string } } ToggleSwitchItem
|
||||
@@ -11,55 +11,55 @@ import { $el } from "../ui";
|
||||
* @param { (e: { item: ToggleSwitchItem, prev?: ToggleSwitchItem }) => void } [opts.onChange]
|
||||
*/
|
||||
export function toggleSwitch(name, items, e?) {
|
||||
const onChange = e?.onChange;
|
||||
const onChange = e?.onChange
|
||||
|
||||
let selectedIndex;
|
||||
let elements;
|
||||
let selectedIndex
|
||||
let elements
|
||||
|
||||
function updateSelected(index) {
|
||||
if (selectedIndex != null) {
|
||||
elements[selectedIndex].classList.remove("comfy-toggle-selected");
|
||||
elements[selectedIndex].classList.remove('comfy-toggle-selected')
|
||||
}
|
||||
onChange?.({
|
||||
item: items[index],
|
||||
prev: selectedIndex == null ? undefined : items[selectedIndex],
|
||||
});
|
||||
selectedIndex = index;
|
||||
elements[selectedIndex].classList.add("comfy-toggle-selected");
|
||||
prev: selectedIndex == null ? undefined : items[selectedIndex]
|
||||
})
|
||||
selectedIndex = index
|
||||
elements[selectedIndex].classList.add('comfy-toggle-selected')
|
||||
}
|
||||
|
||||
elements = items.map((item, i) => {
|
||||
if (typeof item === "string") item = { text: item };
|
||||
if (!item.value) item.value = item.text;
|
||||
if (typeof item === 'string') item = { text: item }
|
||||
if (!item.value) item.value = item.text
|
||||
|
||||
const toggle = $el(
|
||||
"label",
|
||||
'label',
|
||||
{
|
||||
textContent: item.text,
|
||||
title: item.tooltip ?? "",
|
||||
title: item.tooltip ?? ''
|
||||
},
|
||||
$el("input", {
|
||||
$el('input', {
|
||||
name,
|
||||
type: "radio",
|
||||
type: 'radio',
|
||||
value: item.value ?? item.text,
|
||||
checked: item.selected,
|
||||
onchange: () => {
|
||||
updateSelected(i);
|
||||
},
|
||||
})
|
||||
);
|
||||
if (item.selected) {
|
||||
updateSelected(i);
|
||||
updateSelected(i)
|
||||
}
|
||||
return toggle;
|
||||
});
|
||||
})
|
||||
)
|
||||
if (item.selected) {
|
||||
updateSelected(i)
|
||||
}
|
||||
return toggle
|
||||
})
|
||||
|
||||
const container = $el("div.comfy-toggle-switch", elements);
|
||||
const container = $el('div.comfy-toggle-switch', elements)
|
||||
|
||||
if (selectedIndex == null) {
|
||||
elements[0].children[0].checked = true;
|
||||
updateSelected(0);
|
||||
elements[0].children[0].checked = true
|
||||
updateSelected(0)
|
||||
}
|
||||
|
||||
return container;
|
||||
return container
|
||||
}
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
import { api } from "../api";
|
||||
import { $el } from "../ui";
|
||||
import { createSpinner } from "./spinner";
|
||||
import "./userSelection.css";
|
||||
import { api } from '../api'
|
||||
import { $el } from '../ui'
|
||||
import { createSpinner } from './spinner'
|
||||
import './userSelection.css'
|
||||
|
||||
interface SelectedUser {
|
||||
username: string;
|
||||
userId: string;
|
||||
created: boolean;
|
||||
username: string
|
||||
userId: string
|
||||
created: boolean
|
||||
}
|
||||
|
||||
export class UserSelectionScreen {
|
||||
async show(users, user): Promise<SelectedUser> {
|
||||
const userSelection = document.getElementById("comfy-user-selection");
|
||||
userSelection.style.display = "";
|
||||
const userSelection = document.getElementById('comfy-user-selection')
|
||||
userSelection.style.display = ''
|
||||
return new Promise((resolve) => {
|
||||
const input = userSelection.getElementsByTagName("input")[0];
|
||||
const select = userSelection.getElementsByTagName("select")[0];
|
||||
const inputSection = input.closest("section");
|
||||
const selectSection = select.closest("section");
|
||||
const form = userSelection.getElementsByTagName("form")[0];
|
||||
const error = userSelection.getElementsByClassName("comfy-user-error")[0];
|
||||
const input = userSelection.getElementsByTagName('input')[0]
|
||||
const select = userSelection.getElementsByTagName('select')[0]
|
||||
const inputSection = input.closest('section')
|
||||
const selectSection = select.closest('section')
|
||||
const form = userSelection.getElementsByTagName('form')[0]
|
||||
const error = userSelection.getElementsByClassName('comfy-user-error')[0]
|
||||
const button = userSelection.getElementsByClassName(
|
||||
"comfy-user-button-next"
|
||||
)[0];
|
||||
'comfy-user-button-next'
|
||||
)[0]
|
||||
|
||||
let inputActive = null;
|
||||
input.addEventListener("focus", () => {
|
||||
inputSection.classList.add("selected");
|
||||
selectSection.classList.remove("selected");
|
||||
inputActive = true;
|
||||
});
|
||||
select.addEventListener("focus", () => {
|
||||
inputSection.classList.remove("selected");
|
||||
selectSection.classList.add("selected");
|
||||
inputActive = false;
|
||||
select.style.color = "";
|
||||
});
|
||||
select.addEventListener("blur", () => {
|
||||
let inputActive = null
|
||||
input.addEventListener('focus', () => {
|
||||
inputSection.classList.add('selected')
|
||||
selectSection.classList.remove('selected')
|
||||
inputActive = true
|
||||
})
|
||||
select.addEventListener('focus', () => {
|
||||
inputSection.classList.remove('selected')
|
||||
selectSection.classList.add('selected')
|
||||
inputActive = false
|
||||
select.style.color = ''
|
||||
})
|
||||
select.addEventListener('blur', () => {
|
||||
if (!select.value) {
|
||||
select.style.color = "var(--descrip-text)";
|
||||
select.style.color = 'var(--descrip-text)'
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault()
|
||||
if (inputActive == null) {
|
||||
error.textContent =
|
||||
"Please enter a username or select an existing user.";
|
||||
'Please enter a username or select an existing user.'
|
||||
} else if (inputActive) {
|
||||
const username = input.value.trim();
|
||||
const username = input.value.trim()
|
||||
if (!username) {
|
||||
error.textContent = "Please enter a username.";
|
||||
return;
|
||||
error.textContent = 'Please enter a username.'
|
||||
return
|
||||
}
|
||||
|
||||
// Create new user
|
||||
@@ -63,31 +63,31 @@ export class UserSelectionScreen {
|
||||
input.readonly =
|
||||
// @ts-ignore
|
||||
select.readonly =
|
||||
true;
|
||||
const spinner = createSpinner();
|
||||
button.prepend(spinner);
|
||||
true
|
||||
const spinner = createSpinner()
|
||||
button.prepend(spinner)
|
||||
try {
|
||||
const resp = await api.createUser(username);
|
||||
const resp = await api.createUser(username)
|
||||
if (resp.status >= 300) {
|
||||
let message =
|
||||
"Error creating user: " + resp.status + " " + resp.statusText;
|
||||
'Error creating user: ' + resp.status + ' ' + resp.statusText
|
||||
try {
|
||||
const res = await resp.json();
|
||||
const res = await resp.json()
|
||||
if (res.error) {
|
||||
message = res.error;
|
||||
message = res.error
|
||||
}
|
||||
} catch (error) {}
|
||||
throw new Error(message);
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
resolve({ username, userId: await resp.json(), created: true });
|
||||
resolve({ username, userId: await resp.json(), created: true })
|
||||
} catch (err) {
|
||||
spinner.remove();
|
||||
spinner.remove()
|
||||
error.textContent =
|
||||
err.message ??
|
||||
err.statusText ??
|
||||
err ??
|
||||
"An unknown error occurred.";
|
||||
'An unknown error occurred.'
|
||||
// Property 'readonly' does not exist on type 'HTMLSelectElement'.ts(2339)
|
||||
// Property 'readonly' does not exist on type 'HTMLInputElement'. Did you mean 'readOnly'?ts(2551)
|
||||
input.disabled =
|
||||
@@ -96,50 +96,50 @@ export class UserSelectionScreen {
|
||||
input.readonly =
|
||||
// @ts-ignore
|
||||
select.readonly =
|
||||
false;
|
||||
return;
|
||||
false
|
||||
return
|
||||
}
|
||||
} else if (!select.value) {
|
||||
error.textContent = "Please select an existing user.";
|
||||
return;
|
||||
error.textContent = 'Please select an existing user.'
|
||||
return
|
||||
} else {
|
||||
resolve({
|
||||
username: users[select.value],
|
||||
userId: select.value,
|
||||
created: false,
|
||||
});
|
||||
created: false
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
if (user) {
|
||||
const name = localStorage["Comfy.userName"];
|
||||
const name = localStorage['Comfy.userName']
|
||||
if (name) {
|
||||
input.value = name;
|
||||
input.value = name
|
||||
}
|
||||
}
|
||||
if (input.value) {
|
||||
// Focus the input, do this separately as sometimes browsers like to fill in the value
|
||||
input.focus();
|
||||
input.focus()
|
||||
}
|
||||
|
||||
const userIds = Object.keys(users ?? {});
|
||||
const userIds = Object.keys(users ?? {})
|
||||
if (userIds.length) {
|
||||
for (const u of userIds) {
|
||||
$el("option", { textContent: users[u], value: u, parent: select });
|
||||
$el('option', { textContent: users[u], value: u, parent: select })
|
||||
}
|
||||
select.style.color = "var(--descrip-text)";
|
||||
select.style.color = 'var(--descrip-text)'
|
||||
|
||||
if (select.value) {
|
||||
// Focus the select, do this separately as sometimes browsers like to fill in the value
|
||||
select.focus();
|
||||
select.focus()
|
||||
}
|
||||
} else {
|
||||
userSelection.classList.add("no-users");
|
||||
input.focus();
|
||||
userSelection.classList.add('no-users')
|
||||
input.focus()
|
||||
}
|
||||
}).then((r: SelectedUser) => {
|
||||
userSelection.remove();
|
||||
return r;
|
||||
});
|
||||
userSelection.remove()
|
||||
return r
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
export type ClassList = string | string[] | Record<string, boolean>;
|
||||
export type ClassList = string | string[] | Record<string, boolean>
|
||||
|
||||
export function applyClasses(
|
||||
element: HTMLElement,
|
||||
classList: ClassList,
|
||||
...requiredClasses: string[]
|
||||
) {
|
||||
classList ??= "";
|
||||
classList ??= ''
|
||||
|
||||
let str: string;
|
||||
if (typeof classList === "string") {
|
||||
str = classList;
|
||||
let str: string
|
||||
if (typeof classList === 'string') {
|
||||
str = classList
|
||||
} else if (classList instanceof Array) {
|
||||
str = classList.join(" ");
|
||||
str = classList.join(' ')
|
||||
} else {
|
||||
str = Object.entries(classList).reduce((p, c) => {
|
||||
if (c[1]) {
|
||||
p += (p.length ? " " : "") + c[0];
|
||||
p += (p.length ? ' ' : '') + c[0]
|
||||
}
|
||||
return p;
|
||||
}, "");
|
||||
return p
|
||||
}, '')
|
||||
}
|
||||
element.className = str;
|
||||
element.className = str
|
||||
if (requiredClasses) {
|
||||
element.classList.add(...requiredClasses);
|
||||
element.classList.add(...requiredClasses)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,28 +30,28 @@ export function toggleElement(
|
||||
element: HTMLElement,
|
||||
{
|
||||
onHide,
|
||||
onShow,
|
||||
onShow
|
||||
}: {
|
||||
onHide?: (el: HTMLElement) => void;
|
||||
onShow?: (el: HTMLElement, value) => void;
|
||||
onHide?: (el: HTMLElement) => void
|
||||
onShow?: (el: HTMLElement, value) => void
|
||||
} = {}
|
||||
) {
|
||||
let placeholder: HTMLElement | Comment;
|
||||
let hidden: boolean;
|
||||
let placeholder: HTMLElement | Comment
|
||||
let hidden: boolean
|
||||
return (value) => {
|
||||
if (value) {
|
||||
if (hidden) {
|
||||
hidden = false;
|
||||
placeholder.replaceWith(element);
|
||||
hidden = false
|
||||
placeholder.replaceWith(element)
|
||||
}
|
||||
onShow?.(element, value);
|
||||
onShow?.(element, value)
|
||||
} else {
|
||||
if (!placeholder) {
|
||||
placeholder = document.createComment("");
|
||||
placeholder = document.createComment('')
|
||||
}
|
||||
hidden = true
|
||||
element.replaceWith(placeholder)
|
||||
onHide?.(element)
|
||||
}
|
||||
hidden = true;
|
||||
element.replaceWith(placeholder);
|
||||
onHide?.(element);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { api } from "./api";
|
||||
import type { ComfyApp } from "./app";
|
||||
import { $el } from "./ui";
|
||||
import { api } from './api'
|
||||
import type { ComfyApp } from './app'
|
||||
import { $el } from './ui'
|
||||
|
||||
// Simple date formatter
|
||||
const parts = {
|
||||
@@ -8,88 +8,82 @@ const parts = {
|
||||
M: (d) => d.getMonth() + 1,
|
||||
h: (d) => d.getHours(),
|
||||
m: (d) => d.getMinutes(),
|
||||
s: (d) => d.getSeconds(),
|
||||
};
|
||||
s: (d) => d.getSeconds()
|
||||
}
|
||||
const format =
|
||||
Object.keys(parts)
|
||||
.map((k) => k + k + "?")
|
||||
.join("|") + "|yyy?y?";
|
||||
.map((k) => k + k + '?')
|
||||
.join('|') + '|yyy?y?'
|
||||
|
||||
function formatDate(text: string, date: Date) {
|
||||
return text.replace(new RegExp(format, "g"), (text: string): string => {
|
||||
if (text === "yy") return (date.getFullYear() + "").substring(2);
|
||||
if (text === "yyyy") return date.getFullYear().toString();
|
||||
return text.replace(new RegExp(format, 'g'), (text: string): string => {
|
||||
if (text === 'yy') return (date.getFullYear() + '').substring(2)
|
||||
if (text === 'yyyy') return date.getFullYear().toString()
|
||||
if (text[0] in parts) {
|
||||
const p = parts[text[0]](date);
|
||||
return (p + "").padStart(text.length, "0");
|
||||
const p = parts[text[0]](date)
|
||||
return (p + '').padStart(text.length, '0')
|
||||
}
|
||||
return text;
|
||||
});
|
||||
return text
|
||||
})
|
||||
}
|
||||
|
||||
export function clone(obj) {
|
||||
try {
|
||||
if (typeof structuredClone !== "undefined") {
|
||||
return structuredClone(obj);
|
||||
if (typeof structuredClone !== 'undefined') {
|
||||
return structuredClone(obj)
|
||||
}
|
||||
} catch (error) {
|
||||
// structuredClone is stricter than using JSON.parse/stringify so fallback to that
|
||||
}
|
||||
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
return JSON.parse(JSON.stringify(obj))
|
||||
}
|
||||
|
||||
export function applyTextReplacements(app: ComfyApp, value: string): string {
|
||||
return value.replace(/%([^%]+)%/g, function (match, text) {
|
||||
const split = text.split(".");
|
||||
const split = text.split('.')
|
||||
if (split.length !== 2) {
|
||||
// Special handling for dates
|
||||
if (split[0].startsWith("date:")) {
|
||||
return formatDate(split[0].substring(5), new Date());
|
||||
if (split[0].startsWith('date:')) {
|
||||
return formatDate(split[0].substring(5), new Date())
|
||||
}
|
||||
|
||||
if (text !== "width" && text !== "height") {
|
||||
if (text !== 'width' && text !== 'height') {
|
||||
// Dont warn on standard replacements
|
||||
console.warn("Invalid replacement pattern", text);
|
||||
console.warn('Invalid replacement pattern', text)
|
||||
}
|
||||
return match;
|
||||
return match
|
||||
}
|
||||
|
||||
// Find node with matching S&R property name
|
||||
// @ts-ignore
|
||||
let nodes = app.graph._nodes.filter(
|
||||
(n) => n.properties?.["Node name for S&R"] === split[0]
|
||||
);
|
||||
(n) => n.properties?.['Node name for S&R'] === split[0]
|
||||
)
|
||||
// If we cant, see if there is a node with that title
|
||||
if (!nodes.length) {
|
||||
// @ts-ignore
|
||||
nodes = app.graph._nodes.filter((n) => n.title === split[0]);
|
||||
nodes = app.graph._nodes.filter((n) => n.title === split[0])
|
||||
}
|
||||
if (!nodes.length) {
|
||||
console.warn("Unable to find node", split[0]);
|
||||
return match;
|
||||
console.warn('Unable to find node', split[0])
|
||||
return match
|
||||
}
|
||||
|
||||
if (nodes.length > 1) {
|
||||
console.warn("Multiple nodes matched", split[0], "using first match");
|
||||
console.warn('Multiple nodes matched', split[0], 'using first match')
|
||||
}
|
||||
|
||||
const node = nodes[0];
|
||||
const node = nodes[0]
|
||||
|
||||
const widget = node.widgets?.find((w) => w.name === split[1]);
|
||||
const widget = node.widgets?.find((w) => w.name === split[1])
|
||||
if (!widget) {
|
||||
console.warn(
|
||||
"Unable to find widget",
|
||||
split[1],
|
||||
"on node",
|
||||
split[0],
|
||||
node
|
||||
);
|
||||
return match;
|
||||
console.warn('Unable to find widget', split[1], 'on node', split[0], node)
|
||||
return match
|
||||
}
|
||||
|
||||
return ((widget.value ?? "") + "").replaceAll(/\/|\\/g, "_");
|
||||
});
|
||||
return ((widget.value ?? '') + '').replaceAll(/\/|\\/g, '_')
|
||||
})
|
||||
}
|
||||
|
||||
export async function addStylesheet(
|
||||
@@ -97,24 +91,24 @@ export async function addStylesheet(
|
||||
relativeTo?: string
|
||||
): Promise<void> {
|
||||
return new Promise((res, rej) => {
|
||||
let url;
|
||||
if (urlOrFile.endsWith(".js")) {
|
||||
url = urlOrFile.substr(0, urlOrFile.length - 2) + "css";
|
||||
let url
|
||||
if (urlOrFile.endsWith('.js')) {
|
||||
url = urlOrFile.substr(0, urlOrFile.length - 2) + 'css'
|
||||
} else {
|
||||
url = new URL(
|
||||
urlOrFile,
|
||||
relativeTo ?? `${window.location.protocol}//${window.location.host}`
|
||||
).toString();
|
||||
).toString()
|
||||
}
|
||||
$el("link", {
|
||||
$el('link', {
|
||||
parent: document.head,
|
||||
rel: "stylesheet",
|
||||
type: "text/css",
|
||||
rel: 'stylesheet',
|
||||
type: 'text/css',
|
||||
href: url,
|
||||
onload: res,
|
||||
onerror: rej,
|
||||
});
|
||||
});
|
||||
onerror: rej
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,18 +116,18 @@ export async function addStylesheet(
|
||||
* @param { Blob } blob
|
||||
*/
|
||||
export function downloadBlob(filename, blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = $el("a", {
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = $el('a', {
|
||||
href: url,
|
||||
download: filename,
|
||||
style: { display: "none" },
|
||||
parent: document.body,
|
||||
});
|
||||
a.click();
|
||||
style: { display: 'none' },
|
||||
parent: document.body
|
||||
})
|
||||
a.click()
|
||||
setTimeout(function () {
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
a.remove()
|
||||
window.URL.revokeObjectURL(url)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
export function prop<T>(
|
||||
@@ -147,32 +141,32 @@ export function prop<T>(
|
||||
name: string
|
||||
) => void
|
||||
): T {
|
||||
let currentValue;
|
||||
let currentValue
|
||||
Object.defineProperty(target, name, {
|
||||
get() {
|
||||
return currentValue;
|
||||
return currentValue
|
||||
},
|
||||
set(newValue) {
|
||||
const prevValue = currentValue;
|
||||
currentValue = newValue;
|
||||
onChanged?.(currentValue, prevValue, target, name);
|
||||
},
|
||||
});
|
||||
return defaultValue;
|
||||
const prevValue = currentValue
|
||||
currentValue = newValue
|
||||
onChanged?.(currentValue, prevValue, target, name)
|
||||
}
|
||||
})
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
export function getStorageValue(id) {
|
||||
const clientId = api.clientId ?? api.initialClientId;
|
||||
const clientId = api.clientId ?? api.initialClientId
|
||||
return (
|
||||
(clientId && sessionStorage.getItem(`${id}:${clientId}`)) ??
|
||||
localStorage.getItem(id)
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export function setStorageValue(id, value) {
|
||||
const clientId = api.clientId ?? api.initialClientId;
|
||||
const clientId = api.clientId ?? api.initialClientId
|
||||
if (clientId) {
|
||||
sessionStorage.setItem(`${id}:${clientId}`, value);
|
||||
sessionStorage.setItem(`${id}:${clientId}`, value)
|
||||
}
|
||||
localStorage.setItem(id, value);
|
||||
localStorage.setItem(id, value)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { api } from "./api";
|
||||
import "./domWidget";
|
||||
import type { ComfyApp } from "./app";
|
||||
import type { IWidget, LGraphNode } from "@comfyorg/litegraph";
|
||||
import { ComfyNodeDef } from "@/types/apiTypes";
|
||||
import { api } from './api'
|
||||
import './domWidget'
|
||||
import type { ComfyApp } from './app'
|
||||
import type { IWidget, LGraphNode } from '@comfyorg/litegraph'
|
||||
import { ComfyNodeDef } from '@/types/apiTypes'
|
||||
|
||||
export type ComfyWidgetConstructor = (
|
||||
node: LGraphNode,
|
||||
@@ -10,20 +10,20 @@ export type ComfyWidgetConstructor = (
|
||||
inputData: ComfyNodeDef,
|
||||
app?: ComfyApp,
|
||||
widgetName?: string
|
||||
) => { widget: IWidget; minWidth?: number; minHeight?: number };
|
||||
) => { widget: IWidget; minWidth?: number; minHeight?: number }
|
||||
|
||||
let controlValueRunBefore = false;
|
||||
let controlValueRunBefore = false
|
||||
export function updateControlWidgetLabel(widget) {
|
||||
let replacement = "after";
|
||||
let find = "before";
|
||||
let replacement = 'after'
|
||||
let find = 'before'
|
||||
if (controlValueRunBefore) {
|
||||
[find, replacement] = [replacement, find];
|
||||
;[find, replacement] = [replacement, find]
|
||||
}
|
||||
widget.label = (widget.label ?? widget.name).replace(find, replacement);
|
||||
widget.label = (widget.label ?? widget.name).replace(find, replacement)
|
||||
}
|
||||
|
||||
const IS_CONTROL_WIDGET = Symbol();
|
||||
const HAS_EXECUTED = Symbol();
|
||||
const IS_CONTROL_WIDGET = Symbol()
|
||||
const HAS_EXECUTED = Symbol()
|
||||
|
||||
function getNumberDefaults(
|
||||
inputData: ComfyNodeDef,
|
||||
@@ -31,41 +31,41 @@ function getNumberDefaults(
|
||||
precision,
|
||||
enable_rounding
|
||||
) {
|
||||
let defaultVal = inputData[1]["default"];
|
||||
let { min, max, step, round } = inputData[1];
|
||||
let defaultVal = inputData[1]['default']
|
||||
let { min, max, step, round } = inputData[1]
|
||||
|
||||
if (defaultVal == undefined) defaultVal = 0;
|
||||
if (min == undefined) min = 0;
|
||||
if (max == undefined) max = 2048;
|
||||
if (step == undefined) step = defaultStep;
|
||||
if (defaultVal == undefined) defaultVal = 0
|
||||
if (min == undefined) min = 0
|
||||
if (max == undefined) max = 2048
|
||||
if (step == undefined) step = defaultStep
|
||||
// precision is the number of decimal places to show.
|
||||
// by default, display the the smallest number of decimal places such that changes of size step are visible.
|
||||
if (precision == undefined) {
|
||||
precision = Math.max(-Math.floor(Math.log10(step)), 0);
|
||||
precision = Math.max(-Math.floor(Math.log10(step)), 0)
|
||||
}
|
||||
|
||||
if (enable_rounding && (round == undefined || round === true)) {
|
||||
// by default, round the value to those decimal places shown.
|
||||
round = Math.round(1000000 * Math.pow(0.1, precision)) / 1000000;
|
||||
round = Math.round(1000000 * Math.pow(0.1, precision)) / 1000000
|
||||
}
|
||||
|
||||
return {
|
||||
val: defaultVal,
|
||||
config: { min, max, step: 10.0 * step, round, precision },
|
||||
};
|
||||
config: { min, max, step: 10.0 * step, round, precision }
|
||||
}
|
||||
}
|
||||
|
||||
export function addValueControlWidget(
|
||||
node,
|
||||
targetWidget,
|
||||
defaultValue = "randomize",
|
||||
defaultValue = 'randomize',
|
||||
values,
|
||||
widgetName,
|
||||
inputData: ComfyNodeDef
|
||||
) {
|
||||
let name = inputData[1]?.control_after_generate;
|
||||
if (typeof name !== "string") {
|
||||
name = widgetName;
|
||||
let name = inputData[1]?.control_after_generate
|
||||
if (typeof name !== 'string') {
|
||||
name = widgetName
|
||||
}
|
||||
const widgets = addValueControlWidgets(
|
||||
node,
|
||||
@@ -73,200 +73,200 @@ export function addValueControlWidget(
|
||||
defaultValue,
|
||||
{
|
||||
addFilterList: false,
|
||||
controlAfterGenerateName: name,
|
||||
controlAfterGenerateName: name
|
||||
},
|
||||
inputData
|
||||
);
|
||||
return widgets[0];
|
||||
)
|
||||
return widgets[0]
|
||||
}
|
||||
|
||||
export function addValueControlWidgets(
|
||||
node,
|
||||
targetWidget,
|
||||
defaultValue = "randomize",
|
||||
defaultValue = 'randomize',
|
||||
options,
|
||||
inputData: ComfyNodeDef
|
||||
) {
|
||||
if (!defaultValue) defaultValue = "randomize";
|
||||
if (!options) options = {};
|
||||
if (!defaultValue) defaultValue = 'randomize'
|
||||
if (!options) options = {}
|
||||
|
||||
const getName = (defaultName, optionName) => {
|
||||
let name = defaultName;
|
||||
let name = defaultName
|
||||
if (options[optionName]) {
|
||||
name = options[optionName];
|
||||
} else if (typeof inputData?.[1]?.[defaultName] === "string") {
|
||||
name = inputData?.[1]?.[defaultName];
|
||||
name = options[optionName]
|
||||
} else if (typeof inputData?.[1]?.[defaultName] === 'string') {
|
||||
name = inputData?.[1]?.[defaultName]
|
||||
} else if (inputData?.[1]?.control_prefix) {
|
||||
name = inputData?.[1]?.control_prefix + " " + name;
|
||||
name = inputData?.[1]?.control_prefix + ' ' + name
|
||||
}
|
||||
return name
|
||||
}
|
||||
return name;
|
||||
};
|
||||
|
||||
const widgets = [];
|
||||
const widgets = []
|
||||
const valueControl = node.addWidget(
|
||||
"combo",
|
||||
getName("control_after_generate", "controlAfterGenerateName"),
|
||||
'combo',
|
||||
getName('control_after_generate', 'controlAfterGenerateName'),
|
||||
defaultValue,
|
||||
function () {},
|
||||
{
|
||||
values: ["fixed", "increment", "decrement", "randomize"],
|
||||
serialize: false, // Don't include this in prompt.
|
||||
values: ['fixed', 'increment', 'decrement', 'randomize'],
|
||||
serialize: false // Don't include this in prompt.
|
||||
}
|
||||
);
|
||||
valueControl[IS_CONTROL_WIDGET] = true;
|
||||
updateControlWidgetLabel(valueControl);
|
||||
widgets.push(valueControl);
|
||||
)
|
||||
valueControl[IS_CONTROL_WIDGET] = true
|
||||
updateControlWidgetLabel(valueControl)
|
||||
widgets.push(valueControl)
|
||||
|
||||
const isCombo = targetWidget.type === "combo";
|
||||
let comboFilter;
|
||||
const isCombo = targetWidget.type === 'combo'
|
||||
let comboFilter
|
||||
if (isCombo) {
|
||||
valueControl.options.values.push("increment-wrap");
|
||||
valueControl.options.values.push('increment-wrap')
|
||||
}
|
||||
if (isCombo && options.addFilterList !== false) {
|
||||
comboFilter = node.addWidget(
|
||||
"string",
|
||||
getName("control_filter_list", "controlFilterListName"),
|
||||
"",
|
||||
'string',
|
||||
getName('control_filter_list', 'controlFilterListName'),
|
||||
'',
|
||||
function () {},
|
||||
{
|
||||
serialize: false, // Don't include this in prompt.
|
||||
serialize: false // Don't include this in prompt.
|
||||
}
|
||||
);
|
||||
updateControlWidgetLabel(comboFilter);
|
||||
)
|
||||
updateControlWidgetLabel(comboFilter)
|
||||
|
||||
widgets.push(comboFilter);
|
||||
widgets.push(comboFilter)
|
||||
}
|
||||
|
||||
const applyWidgetControl = () => {
|
||||
var v = valueControl.value;
|
||||
var v = valueControl.value
|
||||
|
||||
if (isCombo && v !== "fixed") {
|
||||
let values = targetWidget.options.values;
|
||||
const filter = comboFilter?.value;
|
||||
if (isCombo && v !== 'fixed') {
|
||||
let values = targetWidget.options.values
|
||||
const filter = comboFilter?.value
|
||||
if (filter) {
|
||||
let check;
|
||||
if (filter.startsWith("/") && filter.endsWith("/")) {
|
||||
let check
|
||||
if (filter.startsWith('/') && filter.endsWith('/')) {
|
||||
try {
|
||||
const regex = new RegExp(filter.substring(1, filter.length - 1));
|
||||
check = (item) => regex.test(item);
|
||||
const regex = new RegExp(filter.substring(1, filter.length - 1))
|
||||
check = (item) => regex.test(item)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Error constructing RegExp filter for node " + node.id,
|
||||
'Error constructing RegExp filter for node ' + node.id,
|
||||
filter,
|
||||
error
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!check) {
|
||||
const lower = filter.toLocaleLowerCase();
|
||||
check = (item) => item.toLocaleLowerCase().includes(lower);
|
||||
const lower = filter.toLocaleLowerCase()
|
||||
check = (item) => item.toLocaleLowerCase().includes(lower)
|
||||
}
|
||||
values = values.filter((item) => check(item));
|
||||
values = values.filter((item) => check(item))
|
||||
if (!values.length && targetWidget.options.values.length) {
|
||||
console.warn(
|
||||
"Filter for node " + node.id + " has filtered out all items",
|
||||
'Filter for node ' + node.id + ' has filtered out all items',
|
||||
filter
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
let current_index = values.indexOf(targetWidget.value);
|
||||
let current_length = values.length;
|
||||
let current_index = values.indexOf(targetWidget.value)
|
||||
let current_length = values.length
|
||||
|
||||
switch (v) {
|
||||
case "increment":
|
||||
current_index += 1;
|
||||
break;
|
||||
case "increment-wrap":
|
||||
current_index += 1;
|
||||
case 'increment':
|
||||
current_index += 1
|
||||
break
|
||||
case 'increment-wrap':
|
||||
current_index += 1
|
||||
if (current_index >= current_length) {
|
||||
current_index = 0;
|
||||
current_index = 0
|
||||
}
|
||||
break;
|
||||
case "decrement":
|
||||
current_index -= 1;
|
||||
break;
|
||||
case "randomize":
|
||||
current_index = Math.floor(Math.random() * current_length);
|
||||
break;
|
||||
break
|
||||
case 'decrement':
|
||||
current_index -= 1
|
||||
break
|
||||
case 'randomize':
|
||||
current_index = Math.floor(Math.random() * current_length)
|
||||
break
|
||||
default:
|
||||
break;
|
||||
break
|
||||
}
|
||||
current_index = Math.max(0, current_index);
|
||||
current_index = Math.min(current_length - 1, current_index);
|
||||
current_index = Math.max(0, current_index)
|
||||
current_index = Math.min(current_length - 1, current_index)
|
||||
if (current_index >= 0) {
|
||||
let value = values[current_index];
|
||||
targetWidget.value = value;
|
||||
targetWidget.callback(value);
|
||||
let value = values[current_index]
|
||||
targetWidget.value = value
|
||||
targetWidget.callback(value)
|
||||
}
|
||||
} else {
|
||||
//number
|
||||
let min = targetWidget.options.min;
|
||||
let max = targetWidget.options.max;
|
||||
let min = targetWidget.options.min
|
||||
let max = targetWidget.options.max
|
||||
// limit to something that javascript can handle
|
||||
max = Math.min(1125899906842624, max);
|
||||
min = Math.max(-1125899906842624, min);
|
||||
let range = (max - min) / (targetWidget.options.step / 10);
|
||||
max = Math.min(1125899906842624, max)
|
||||
min = Math.max(-1125899906842624, min)
|
||||
let range = (max - min) / (targetWidget.options.step / 10)
|
||||
|
||||
//adjust values based on valueControl Behaviour
|
||||
switch (v) {
|
||||
case "fixed":
|
||||
break;
|
||||
case "increment":
|
||||
targetWidget.value += targetWidget.options.step / 10;
|
||||
break;
|
||||
case "decrement":
|
||||
targetWidget.value -= targetWidget.options.step / 10;
|
||||
break;
|
||||
case "randomize":
|
||||
case 'fixed':
|
||||
break
|
||||
case 'increment':
|
||||
targetWidget.value += targetWidget.options.step / 10
|
||||
break
|
||||
case 'decrement':
|
||||
targetWidget.value -= targetWidget.options.step / 10
|
||||
break
|
||||
case 'randomize':
|
||||
targetWidget.value =
|
||||
Math.floor(Math.random() * range) *
|
||||
(targetWidget.options.step / 10) +
|
||||
min;
|
||||
break;
|
||||
min
|
||||
break
|
||||
default:
|
||||
break;
|
||||
break
|
||||
}
|
||||
/*check if values are over or under their respective
|
||||
* ranges and set them to min or max.*/
|
||||
if (targetWidget.value < min) targetWidget.value = min;
|
||||
if (targetWidget.value < min) targetWidget.value = min
|
||||
|
||||
if (targetWidget.value > max) targetWidget.value = max;
|
||||
targetWidget.callback(targetWidget.value);
|
||||
if (targetWidget.value > max) targetWidget.value = max
|
||||
targetWidget.callback(targetWidget.value)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
valueControl.beforeQueued = () => {
|
||||
if (controlValueRunBefore) {
|
||||
// Don't run on first execution
|
||||
if (valueControl[HAS_EXECUTED]) {
|
||||
applyWidgetControl();
|
||||
applyWidgetControl()
|
||||
}
|
||||
}
|
||||
valueControl[HAS_EXECUTED] = true;
|
||||
};
|
||||
valueControl[HAS_EXECUTED] = true
|
||||
}
|
||||
|
||||
valueControl.afterQueued = () => {
|
||||
if (!controlValueRunBefore) {
|
||||
applyWidgetControl();
|
||||
applyWidgetControl()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return widgets;
|
||||
return widgets
|
||||
}
|
||||
|
||||
function seedWidget(node, inputName, inputData: ComfyNodeDef, app, widgetName) {
|
||||
const seed = createIntWidget(node, inputName, inputData, app, true);
|
||||
const seed = createIntWidget(node, inputName, inputData, app, true)
|
||||
const seedControl = addValueControlWidget(
|
||||
node,
|
||||
seed.widget,
|
||||
"randomize",
|
||||
'randomize',
|
||||
undefined,
|
||||
widgetName,
|
||||
inputData
|
||||
);
|
||||
)
|
||||
|
||||
seed.widget.linkedWidgets = [seedControl];
|
||||
return seed;
|
||||
seed.widget.linkedWidgets = [seedControl]
|
||||
return seed
|
||||
}
|
||||
|
||||
function createIntWidget(
|
||||
@@ -276,119 +276,116 @@ function createIntWidget(
|
||||
app,
|
||||
isSeedInput: boolean = false
|
||||
) {
|
||||
const control = inputData[1]?.control_after_generate;
|
||||
const control = inputData[1]?.control_after_generate
|
||||
if (!isSeedInput && control) {
|
||||
return seedWidget(
|
||||
node,
|
||||
inputName,
|
||||
inputData,
|
||||
app,
|
||||
typeof control === "string" ? control : undefined
|
||||
);
|
||||
typeof control === 'string' ? control : undefined
|
||||
)
|
||||
}
|
||||
|
||||
let widgetType = isSlider(inputData[1]["display"], app);
|
||||
const { val, config } = getNumberDefaults(inputData, 1, 0, true);
|
||||
Object.assign(config, { precision: 0 });
|
||||
let widgetType = isSlider(inputData[1]['display'], app)
|
||||
const { val, config } = getNumberDefaults(inputData, 1, 0, true)
|
||||
Object.assign(config, { precision: 0 })
|
||||
return {
|
||||
widget: node.addWidget(
|
||||
widgetType,
|
||||
inputName,
|
||||
val,
|
||||
function (v) {
|
||||
const s = this.options.step / 10;
|
||||
let sh = this.options.min % s;
|
||||
const s = this.options.step / 10
|
||||
let sh = this.options.min % s
|
||||
if (isNaN(sh)) {
|
||||
sh = 0;
|
||||
sh = 0
|
||||
}
|
||||
this.value = Math.round((v - sh) / s) * s + sh;
|
||||
this.value = Math.round((v - sh) / s) * s + sh
|
||||
},
|
||||
config
|
||||
),
|
||||
};
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function addMultilineWidget(node, name, opts, app) {
|
||||
const inputEl = document.createElement("textarea");
|
||||
inputEl.className = "comfy-multiline-input";
|
||||
inputEl.value = opts.defaultVal;
|
||||
inputEl.placeholder = opts.placeholder || name;
|
||||
const inputEl = document.createElement('textarea')
|
||||
inputEl.className = 'comfy-multiline-input'
|
||||
inputEl.value = opts.defaultVal
|
||||
inputEl.placeholder = opts.placeholder || name
|
||||
|
||||
const widget = node.addDOMWidget(name, "customtext", inputEl, {
|
||||
const widget = node.addDOMWidget(name, 'customtext', inputEl, {
|
||||
getValue() {
|
||||
return inputEl.value;
|
||||
return inputEl.value
|
||||
},
|
||||
setValue(v) {
|
||||
inputEl.value = v;
|
||||
},
|
||||
});
|
||||
widget.inputEl = inputEl;
|
||||
inputEl.value = v
|
||||
}
|
||||
})
|
||||
widget.inputEl = inputEl
|
||||
|
||||
inputEl.addEventListener("input", () => {
|
||||
widget.callback?.(widget.value);
|
||||
});
|
||||
inputEl.addEventListener('input', () => {
|
||||
widget.callback?.(widget.value)
|
||||
})
|
||||
|
||||
return { minWidth: 400, minHeight: 200, widget };
|
||||
return { minWidth: 400, minHeight: 200, widget }
|
||||
}
|
||||
|
||||
function isSlider(display, app) {
|
||||
if (app.ui.settings.getSettingValue("Comfy.DisableSliders")) {
|
||||
return "number";
|
||||
if (app.ui.settings.getSettingValue('Comfy.DisableSliders')) {
|
||||
return 'number'
|
||||
}
|
||||
|
||||
return display === "slider" ? "slider" : "number";
|
||||
return display === 'slider' ? 'slider' : 'number'
|
||||
}
|
||||
|
||||
export function initWidgets(app) {
|
||||
app.ui.settings.addSetting({
|
||||
id: "Comfy.WidgetControlMode",
|
||||
name: "Widget Value Control Mode",
|
||||
type: "combo",
|
||||
defaultValue: "after",
|
||||
options: ["before", "after"],
|
||||
id: 'Comfy.WidgetControlMode',
|
||||
name: 'Widget Value Control Mode',
|
||||
type: 'combo',
|
||||
defaultValue: 'after',
|
||||
options: ['before', 'after'],
|
||||
tooltip:
|
||||
"Controls when widget values are updated (randomize/increment/decrement), either before the prompt is queued or after.",
|
||||
'Controls when widget values are updated (randomize/increment/decrement), either before the prompt is queued or after.',
|
||||
onChange(value) {
|
||||
controlValueRunBefore = value === "before";
|
||||
controlValueRunBefore = value === 'before'
|
||||
for (const n of app.graph._nodes) {
|
||||
if (!n.widgets) continue;
|
||||
if (!n.widgets) continue
|
||||
for (const w of n.widgets) {
|
||||
if (w[IS_CONTROL_WIDGET]) {
|
||||
updateControlWidgetLabel(w);
|
||||
updateControlWidgetLabel(w)
|
||||
if (w.linkedWidgets) {
|
||||
for (const l of w.linkedWidgets) {
|
||||
updateControlWidgetLabel(l);
|
||||
updateControlWidgetLabel(l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
app.graph.setDirtyCanvas(true);
|
||||
},
|
||||
});
|
||||
app.graph.setDirtyCanvas(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
||||
"INT:seed": seedWidget,
|
||||
"INT:noise_seed": seedWidget,
|
||||
'INT:seed': seedWidget,
|
||||
'INT:noise_seed': seedWidget,
|
||||
FLOAT(node, inputName, inputData: ComfyNodeDef, app) {
|
||||
let widgetType: "number" | "slider" = isSlider(
|
||||
inputData[1]["display"],
|
||||
app
|
||||
);
|
||||
let widgetType: 'number' | 'slider' = isSlider(inputData[1]['display'], app)
|
||||
let precision = app.ui.settings.getSettingValue(
|
||||
"Comfy.FloatRoundingPrecision"
|
||||
);
|
||||
'Comfy.FloatRoundingPrecision'
|
||||
)
|
||||
let disable_rounding = app.ui.settings.getSettingValue(
|
||||
"Comfy.DisableFloatRounding"
|
||||
);
|
||||
if (precision == 0) precision = undefined;
|
||||
'Comfy.DisableFloatRounding'
|
||||
)
|
||||
if (precision == 0) precision = undefined
|
||||
const { val, config } = getNumberDefaults(
|
||||
inputData,
|
||||
0.5,
|
||||
precision,
|
||||
!disable_rounding
|
||||
);
|
||||
)
|
||||
return {
|
||||
widget: node.addWidget(
|
||||
widgetType,
|
||||
@@ -397,72 +394,66 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
||||
function (v) {
|
||||
if (config.round) {
|
||||
this.value =
|
||||
Math.round((v + Number.EPSILON) / config.round) * config.round;
|
||||
if (this.value > config.max) this.value = config.max;
|
||||
if (this.value < config.min) this.value = config.min;
|
||||
Math.round((v + Number.EPSILON) / config.round) * config.round
|
||||
if (this.value > config.max) this.value = config.max
|
||||
if (this.value < config.min) this.value = config.min
|
||||
} else {
|
||||
this.value = v;
|
||||
this.value = v
|
||||
}
|
||||
},
|
||||
config
|
||||
),
|
||||
};
|
||||
)
|
||||
}
|
||||
},
|
||||
INT(node, inputName, inputData: ComfyNodeDef, app) {
|
||||
return createIntWidget(node, inputName, inputData, app);
|
||||
return createIntWidget(node, inputName, inputData, app)
|
||||
},
|
||||
BOOLEAN(node, inputName, inputData) {
|
||||
let defaultVal = false;
|
||||
let options = {};
|
||||
let defaultVal = false
|
||||
let options = {}
|
||||
if (inputData[1]) {
|
||||
if (inputData[1].default) defaultVal = inputData[1].default;
|
||||
if (inputData[1].label_on) options["on"] = inputData[1].label_on;
|
||||
if (inputData[1].label_off) options["off"] = inputData[1].label_off;
|
||||
if (inputData[1].default) defaultVal = inputData[1].default
|
||||
if (inputData[1].label_on) options['on'] = inputData[1].label_on
|
||||
if (inputData[1].label_off) options['off'] = inputData[1].label_off
|
||||
}
|
||||
return {
|
||||
widget: node.addWidget(
|
||||
"toggle",
|
||||
inputName,
|
||||
defaultVal,
|
||||
() => {},
|
||||
options
|
||||
),
|
||||
};
|
||||
widget: node.addWidget('toggle', inputName, defaultVal, () => {}, options)
|
||||
}
|
||||
},
|
||||
STRING(node, inputName, inputData: ComfyNodeDef, app) {
|
||||
const defaultVal = inputData[1].default || "";
|
||||
const multiline = !!inputData[1].multiline;
|
||||
const defaultVal = inputData[1].default || ''
|
||||
const multiline = !!inputData[1].multiline
|
||||
|
||||
let res;
|
||||
let res
|
||||
if (multiline) {
|
||||
res = addMultilineWidget(
|
||||
node,
|
||||
inputName,
|
||||
{ defaultVal, ...inputData[1] },
|
||||
app
|
||||
);
|
||||
)
|
||||
} else {
|
||||
res = {
|
||||
widget: node.addWidget("text", inputName, defaultVal, () => {}, {}),
|
||||
};
|
||||
widget: node.addWidget('text', inputName, defaultVal, () => {}, {})
|
||||
}
|
||||
}
|
||||
|
||||
if (inputData[1].dynamicPrompts != undefined)
|
||||
res.widget.dynamicPrompts = inputData[1].dynamicPrompts;
|
||||
res.widget.dynamicPrompts = inputData[1].dynamicPrompts
|
||||
|
||||
return res;
|
||||
return res
|
||||
},
|
||||
COMBO(node, inputName, inputData: ComfyNodeDef) {
|
||||
const type = inputData[0];
|
||||
let defaultValue = type[0];
|
||||
const type = inputData[0]
|
||||
let defaultValue = type[0]
|
||||
if (inputData[1] && inputData[1].default) {
|
||||
defaultValue = inputData[1].default;
|
||||
defaultValue = inputData[1].default
|
||||
}
|
||||
const res = {
|
||||
widget: node.addWidget("combo", inputName, defaultValue, () => {}, {
|
||||
values: type,
|
||||
}),
|
||||
};
|
||||
widget: node.addWidget('combo', inputName, defaultValue, () => {}, {
|
||||
values: type
|
||||
})
|
||||
}
|
||||
if (inputData[1]?.control_after_generate) {
|
||||
// TODO make combo handle a widget node type?
|
||||
// @ts-ignore
|
||||
@@ -472,9 +463,9 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
||||
undefined,
|
||||
undefined,
|
||||
inputData
|
||||
);
|
||||
)
|
||||
}
|
||||
return res;
|
||||
return res
|
||||
},
|
||||
IMAGEUPLOAD(
|
||||
node: LGraphNode,
|
||||
@@ -485,168 +476,168 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
||||
// TODO make image upload handle a custom node type?
|
||||
// @ts-ignore
|
||||
const imageWidget = node.widgets.find(
|
||||
(w) => w.name === (inputData[1]?.widget ?? "image")
|
||||
);
|
||||
let uploadWidget;
|
||||
(w) => w.name === (inputData[1]?.widget ?? 'image')
|
||||
)
|
||||
let uploadWidget
|
||||
|
||||
function showImage(name) {
|
||||
const img = new Image();
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
// @ts-ignore
|
||||
node.imgs = [img];
|
||||
app.graph.setDirtyCanvas(true);
|
||||
};
|
||||
let folder_separator = name.lastIndexOf("/");
|
||||
let subfolder = "";
|
||||
node.imgs = [img]
|
||||
app.graph.setDirtyCanvas(true)
|
||||
}
|
||||
let folder_separator = name.lastIndexOf('/')
|
||||
let subfolder = ''
|
||||
if (folder_separator > -1) {
|
||||
subfolder = name.substring(0, folder_separator);
|
||||
name = name.substring(folder_separator + 1);
|
||||
subfolder = name.substring(0, folder_separator)
|
||||
name = name.substring(folder_separator + 1)
|
||||
}
|
||||
img.src = api.apiURL(
|
||||
`/view?filename=${encodeURIComponent(name)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`
|
||||
);
|
||||
)
|
||||
// @ts-ignore
|
||||
node.setSizeForImage?.();
|
||||
node.setSizeForImage?.()
|
||||
}
|
||||
|
||||
var default_value = imageWidget.value;
|
||||
Object.defineProperty(imageWidget, "value", {
|
||||
var default_value = imageWidget.value
|
||||
Object.defineProperty(imageWidget, 'value', {
|
||||
set: function (value) {
|
||||
this._real_value = value;
|
||||
this._real_value = value
|
||||
},
|
||||
|
||||
get: function () {
|
||||
if (!this._real_value) {
|
||||
return default_value;
|
||||
return default_value
|
||||
}
|
||||
|
||||
let value = this._real_value;
|
||||
let value = this._real_value
|
||||
if (value.filename) {
|
||||
let real_value = value;
|
||||
value = "";
|
||||
let real_value = value
|
||||
value = ''
|
||||
if (real_value.subfolder) {
|
||||
value = real_value.subfolder + "/";
|
||||
value = real_value.subfolder + '/'
|
||||
}
|
||||
|
||||
value += real_value.filename;
|
||||
value += real_value.filename
|
||||
|
||||
if (real_value.type && real_value.type !== "input")
|
||||
value += ` [${real_value.type}]`;
|
||||
if (real_value.type && real_value.type !== 'input')
|
||||
value += ` [${real_value.type}]`
|
||||
}
|
||||
return value;
|
||||
},
|
||||
});
|
||||
return value
|
||||
}
|
||||
})
|
||||
|
||||
// Add our own callback to the combo widget to render an image when it changes
|
||||
// TODO: Explain this?
|
||||
// @ts-ignore
|
||||
const cb = node.callback;
|
||||
const cb = node.callback
|
||||
imageWidget.callback = function () {
|
||||
showImage(imageWidget.value);
|
||||
showImage(imageWidget.value)
|
||||
if (cb) {
|
||||
return cb.apply(this, arguments);
|
||||
return cb.apply(this, arguments)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// On load if we have a value then render the image
|
||||
// The value isnt set immediately so we need to wait a moment
|
||||
// No change callbacks seem to be fired on initial setting of the value
|
||||
requestAnimationFrame(() => {
|
||||
if (imageWidget.value) {
|
||||
showImage(imageWidget.value);
|
||||
showImage(imageWidget.value)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
async function uploadFile(file, updateNode, pasted = false) {
|
||||
try {
|
||||
// Wrap file in formdata so it includes filename
|
||||
const body = new FormData();
|
||||
body.append("image", file);
|
||||
if (pasted) body.append("subfolder", "pasted");
|
||||
const resp = await api.fetchApi("/upload/image", {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
const body = new FormData()
|
||||
body.append('image', file)
|
||||
if (pasted) body.append('subfolder', 'pasted')
|
||||
const resp = await api.fetchApi('/upload/image', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
|
||||
if (resp.status === 200) {
|
||||
const data = await resp.json();
|
||||
const data = await resp.json()
|
||||
// Add the file to the dropdown list and update the widget value
|
||||
let path = data.name;
|
||||
if (data.subfolder) path = data.subfolder + "/" + path;
|
||||
let path = data.name
|
||||
if (data.subfolder) path = data.subfolder + '/' + path
|
||||
|
||||
if (!imageWidget.options.values.includes(path)) {
|
||||
imageWidget.options.values.push(path);
|
||||
imageWidget.options.values.push(path)
|
||||
}
|
||||
|
||||
if (updateNode) {
|
||||
showImage(path);
|
||||
imageWidget.value = path;
|
||||
showImage(path)
|
||||
imageWidget.value = path
|
||||
}
|
||||
} else {
|
||||
alert(resp.status + " - " + resp.statusText);
|
||||
alert(resp.status + ' - ' + resp.statusText)
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error);
|
||||
alert(error)
|
||||
}
|
||||
}
|
||||
|
||||
const fileInput = document.createElement("input");
|
||||
const fileInput = document.createElement('input')
|
||||
Object.assign(fileInput, {
|
||||
type: "file",
|
||||
accept: "image/jpeg,image/png,image/webp",
|
||||
style: "display: none",
|
||||
type: 'file',
|
||||
accept: 'image/jpeg,image/png,image/webp',
|
||||
style: 'display: none',
|
||||
onchange: async () => {
|
||||
if (fileInput.files.length) {
|
||||
await uploadFile(fileInput.files[0], true);
|
||||
await uploadFile(fileInput.files[0], true)
|
||||
}
|
||||
},
|
||||
});
|
||||
document.body.append(fileInput);
|
||||
}
|
||||
})
|
||||
document.body.append(fileInput)
|
||||
|
||||
// Create the button widget for selecting the files
|
||||
uploadWidget = node.addWidget("button", inputName, "image", () => {
|
||||
fileInput.click();
|
||||
});
|
||||
uploadWidget.label = "choose file to upload";
|
||||
uploadWidget.serialize = false;
|
||||
uploadWidget = node.addWidget('button', inputName, 'image', () => {
|
||||
fileInput.click()
|
||||
})
|
||||
uploadWidget.label = 'choose file to upload'
|
||||
uploadWidget.serialize = false
|
||||
|
||||
// Add handler to check if an image is being dragged over our node
|
||||
// @ts-ignore
|
||||
node.onDragOver = function (e) {
|
||||
if (e.dataTransfer && e.dataTransfer.items) {
|
||||
const image = [...e.dataTransfer.items].find((f) => f.kind === "file");
|
||||
return !!image;
|
||||
const image = [...e.dataTransfer.items].find((f) => f.kind === 'file')
|
||||
return !!image
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
return false
|
||||
}
|
||||
|
||||
// On drop upload files
|
||||
// @ts-ignore
|
||||
node.onDragDrop = function (e) {
|
||||
console.log("onDragDrop called");
|
||||
let handled = false;
|
||||
console.log('onDragDrop called')
|
||||
let handled = false
|
||||
for (const file of e.dataTransfer.files) {
|
||||
if (file.type.startsWith("image/")) {
|
||||
uploadFile(file, !handled); // Dont await these, any order is fine, only update on first one
|
||||
handled = true;
|
||||
if (file.type.startsWith('image/')) {
|
||||
uploadFile(file, !handled) // Dont await these, any order is fine, only update on first one
|
||||
handled = true
|
||||
}
|
||||
}
|
||||
|
||||
return handled;
|
||||
};
|
||||
return handled
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
node.pasteFile = function (file) {
|
||||
if (file.type.startsWith("image/")) {
|
||||
if (file.type.startsWith('image/')) {
|
||||
const is_pasted =
|
||||
file.name === "image.png" && file.lastModified - Date.now() < 2000;
|
||||
uploadFile(file, true, is_pasted);
|
||||
return true;
|
||||
file.name === 'image.png' && file.lastModified - Date.now() < 2000
|
||||
uploadFile(file, true, is_pasted)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return { widget: uploadWidget };
|
||||
},
|
||||
};
|
||||
return { widget: uploadWidget }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
import type { ComfyApp } from "./app";
|
||||
import { api } from "./api";
|
||||
import { ChangeTracker } from "./changeTracker";
|
||||
import { ComfyAsyncDialog } from "./ui/components/asyncDialog";
|
||||
import { getStorageValue, setStorageValue } from "./utils";
|
||||
import { LGraphCanvas, LGraph } from "@comfyorg/litegraph";
|
||||
import type { ComfyApp } from './app'
|
||||
import { api } from './api'
|
||||
import { ChangeTracker } from './changeTracker'
|
||||
import { ComfyAsyncDialog } from './ui/components/asyncDialog'
|
||||
import { getStorageValue, setStorageValue } from './utils'
|
||||
import { LGraphCanvas, LGraph } from '@comfyorg/litegraph'
|
||||
|
||||
function appendJsonExt(path: string) {
|
||||
if (!path.toLowerCase().endsWith(".json")) {
|
||||
path += ".json";
|
||||
if (!path.toLowerCase().endsWith('.json')) {
|
||||
path += '.json'
|
||||
}
|
||||
return path;
|
||||
return path
|
||||
}
|
||||
|
||||
export function trimJsonExt(path: string) {
|
||||
return path?.replace(/\.json$/, "");
|
||||
return path?.replace(/\.json$/, '')
|
||||
}
|
||||
|
||||
export class ComfyWorkflowManager extends EventTarget {
|
||||
#activePromptId: string | null = null;
|
||||
#unsavedCount = 0;
|
||||
#activeWorkflow: ComfyWorkflow;
|
||||
#activePromptId: string | null = null
|
||||
#unsavedCount = 0
|
||||
#activeWorkflow: ComfyWorkflow
|
||||
|
||||
workflowLookup: Record<string, ComfyWorkflow> = {};
|
||||
workflows: Array<ComfyWorkflow> = [];
|
||||
openWorkflows: Array<ComfyWorkflow> = [];
|
||||
workflowLookup: Record<string, ComfyWorkflow> = {}
|
||||
workflows: Array<ComfyWorkflow> = []
|
||||
openWorkflows: Array<ComfyWorkflow> = []
|
||||
queuedPrompts: Record<
|
||||
string,
|
||||
{ workflow?: ComfyWorkflow; nodes?: Record<string, boolean> }
|
||||
> = {};
|
||||
app: ComfyApp;
|
||||
> = {}
|
||||
app: ComfyApp
|
||||
|
||||
get activeWorkflow() {
|
||||
return this.#activeWorkflow ?? this.openWorkflows[0];
|
||||
return this.#activeWorkflow ?? this.openWorkflows[0]
|
||||
}
|
||||
|
||||
get activePromptId() {
|
||||
return this.#activePromptId;
|
||||
return this.#activePromptId
|
||||
}
|
||||
|
||||
get activePrompt() {
|
||||
return this.queuedPrompts[this.#activePromptId];
|
||||
return this.queuedPrompts[this.#activePromptId]
|
||||
}
|
||||
|
||||
constructor(app: ComfyApp) {
|
||||
super();
|
||||
this.app = app;
|
||||
ChangeTracker.init(app);
|
||||
super()
|
||||
this.app = app
|
||||
ChangeTracker.init(app)
|
||||
|
||||
this.#bindExecutionEvents();
|
||||
this.#bindExecutionEvents()
|
||||
}
|
||||
|
||||
#bindExecutionEvents() {
|
||||
@@ -55,99 +55,99 @@ export class ComfyWorkflowManager extends EventTarget {
|
||||
|
||||
const emit = () =>
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("execute", { detail: this.activePrompt })
|
||||
);
|
||||
let executing = null;
|
||||
api.addEventListener("execution_start", (e) => {
|
||||
this.#activePromptId = e.detail.prompt_id;
|
||||
new CustomEvent('execute', { detail: this.activePrompt })
|
||||
)
|
||||
let executing = null
|
||||
api.addEventListener('execution_start', (e) => {
|
||||
this.#activePromptId = e.detail.prompt_id
|
||||
|
||||
// This event can fire before the event is stored, so put a placeholder
|
||||
this.queuedPrompts[this.#activePromptId] ??= { nodes: {} };
|
||||
emit();
|
||||
});
|
||||
api.addEventListener("execution_cached", (e) => {
|
||||
if (!this.activePrompt) return;
|
||||
this.queuedPrompts[this.#activePromptId] ??= { nodes: {} }
|
||||
emit()
|
||||
})
|
||||
api.addEventListener('execution_cached', (e) => {
|
||||
if (!this.activePrompt) return
|
||||
for (const n of e.detail.nodes) {
|
||||
this.activePrompt.nodes[n] = true;
|
||||
this.activePrompt.nodes[n] = true
|
||||
}
|
||||
emit();
|
||||
});
|
||||
api.addEventListener("executed", (e) => {
|
||||
if (!this.activePrompt) return;
|
||||
this.activePrompt.nodes[e.detail.node] = true;
|
||||
emit();
|
||||
});
|
||||
api.addEventListener("executing", (e) => {
|
||||
if (!this.activePrompt) return;
|
||||
emit()
|
||||
})
|
||||
api.addEventListener('executed', (e) => {
|
||||
if (!this.activePrompt) return
|
||||
this.activePrompt.nodes[e.detail.node] = true
|
||||
emit()
|
||||
})
|
||||
api.addEventListener('executing', (e) => {
|
||||
if (!this.activePrompt) return
|
||||
|
||||
if (executing) {
|
||||
// Seems sometimes nodes that are cached fire executing but not executed
|
||||
this.activePrompt.nodes[executing] = true;
|
||||
this.activePrompt.nodes[executing] = true
|
||||
}
|
||||
executing = e.detail;
|
||||
executing = e.detail
|
||||
if (!executing) {
|
||||
delete this.queuedPrompts[this.#activePromptId];
|
||||
this.#activePromptId = null;
|
||||
delete this.queuedPrompts[this.#activePromptId]
|
||||
this.#activePromptId = null
|
||||
}
|
||||
emit();
|
||||
});
|
||||
emit()
|
||||
})
|
||||
}
|
||||
|
||||
async loadWorkflows() {
|
||||
try {
|
||||
let favorites;
|
||||
const resp = await api.getUserData("workflows/.index.json");
|
||||
let info;
|
||||
let favorites
|
||||
const resp = await api.getUserData('workflows/.index.json')
|
||||
let info
|
||||
if (resp.status === 200) {
|
||||
info = await resp.json();
|
||||
favorites = new Set(info?.favorites ?? []);
|
||||
info = await resp.json()
|
||||
favorites = new Set(info?.favorites ?? [])
|
||||
} else {
|
||||
favorites = new Set();
|
||||
favorites = new Set()
|
||||
}
|
||||
|
||||
const workflows = (await api.listUserData("workflows", true, true)).map(
|
||||
const workflows = (await api.listUserData('workflows', true, true)).map(
|
||||
(w) => {
|
||||
let workflow = this.workflowLookup[w[0]];
|
||||
let workflow = this.workflowLookup[w[0]]
|
||||
if (!workflow) {
|
||||
workflow = new ComfyWorkflow(
|
||||
this,
|
||||
w[0],
|
||||
w.slice(1),
|
||||
favorites.has(w[0])
|
||||
);
|
||||
this.workflowLookup[workflow.path] = workflow;
|
||||
)
|
||||
this.workflowLookup[workflow.path] = workflow
|
||||
}
|
||||
return workflow;
|
||||
return workflow
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
this.workflows = workflows;
|
||||
this.workflows = workflows
|
||||
} catch (error) {
|
||||
alert("Error loading workflows: " + (error.message ?? error));
|
||||
this.workflows = [];
|
||||
alert('Error loading workflows: ' + (error.message ?? error))
|
||||
this.workflows = []
|
||||
}
|
||||
}
|
||||
|
||||
async saveWorkflowMetadata() {
|
||||
await api.storeUserData("workflows/.index.json", {
|
||||
await api.storeUserData('workflows/.index.json', {
|
||||
favorites: [
|
||||
...this.workflows.filter((w) => w.isFavorite).map((w) => w.path),
|
||||
],
|
||||
});
|
||||
...this.workflows.filter((w) => w.isFavorite).map((w) => w.path)
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | ComfyWorkflow | null} workflow
|
||||
*/
|
||||
setWorkflow(workflow) {
|
||||
if (workflow && typeof workflow === "string") {
|
||||
if (workflow && typeof workflow === 'string') {
|
||||
// Selected by path, i.e. on reload of last workflow
|
||||
const found = this.workflows.find((w) => w.path === workflow);
|
||||
const found = this.workflows.find((w) => w.path === workflow)
|
||||
if (found) {
|
||||
workflow = found;
|
||||
workflow = found
|
||||
workflow.unsaved =
|
||||
!workflow ||
|
||||
getStorageValue("Comfy.PreviousWorkflowUnsaved") === "true";
|
||||
getStorageValue('Comfy.PreviousWorkflowUnsaved') === 'true'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,33 +156,33 @@ export class ComfyWorkflowManager extends EventTarget {
|
||||
workflow = new ComfyWorkflow(
|
||||
this,
|
||||
workflow ||
|
||||
"Unsaved Workflow" +
|
||||
(this.#unsavedCount++ ? ` (${this.#unsavedCount})` : "")
|
||||
);
|
||||
'Unsaved Workflow' +
|
||||
(this.#unsavedCount++ ? ` (${this.#unsavedCount})` : '')
|
||||
)
|
||||
}
|
||||
|
||||
const index = this.openWorkflows.indexOf(workflow);
|
||||
const index = this.openWorkflows.indexOf(workflow)
|
||||
if (index === -1) {
|
||||
// Opening a new workflow
|
||||
this.openWorkflows.push(workflow);
|
||||
this.openWorkflows.push(workflow)
|
||||
}
|
||||
|
||||
this.#activeWorkflow = workflow;
|
||||
this.#activeWorkflow = workflow
|
||||
|
||||
setStorageValue("Comfy.PreviousWorkflow", this.activeWorkflow.path ?? "");
|
||||
this.dispatchEvent(new CustomEvent("changeWorkflow"));
|
||||
setStorageValue('Comfy.PreviousWorkflow', this.activeWorkflow.path ?? '')
|
||||
this.dispatchEvent(new CustomEvent('changeWorkflow'))
|
||||
}
|
||||
|
||||
storePrompt({ nodes, id }) {
|
||||
this.queuedPrompts[id] ??= {};
|
||||
this.queuedPrompts[id] ??= {}
|
||||
this.queuedPrompts[id].nodes = {
|
||||
...nodes.reduce((p, n) => {
|
||||
p[n] = false;
|
||||
return p;
|
||||
p[n] = false
|
||||
return p
|
||||
}, {}),
|
||||
...this.queuedPrompts[id].nodes,
|
||||
};
|
||||
this.queuedPrompts[id].workflow = this.activeWorkflow;
|
||||
...this.queuedPrompts[id].nodes
|
||||
}
|
||||
this.queuedPrompts[id].workflow = this.activeWorkflow
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,71 +190,71 @@ export class ComfyWorkflowManager extends EventTarget {
|
||||
*/
|
||||
async closeWorkflow(workflow, warnIfUnsaved = true) {
|
||||
if (!workflow.isOpen) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
if (workflow.unsaved && warnIfUnsaved) {
|
||||
const res = await ComfyAsyncDialog.prompt({
|
||||
title: "Save Changes?",
|
||||
title: 'Save Changes?',
|
||||
message: `Do you want to save changes to "${workflow.path ?? workflow.name}" before closing?`,
|
||||
actions: ["Yes", "No", "Cancel"],
|
||||
});
|
||||
if (res === "Yes") {
|
||||
const active = this.activeWorkflow;
|
||||
actions: ['Yes', 'No', 'Cancel']
|
||||
})
|
||||
if (res === 'Yes') {
|
||||
const active = this.activeWorkflow
|
||||
if (active !== workflow) {
|
||||
// We need to switch to the workflow to save it
|
||||
await workflow.load();
|
||||
await workflow.load()
|
||||
}
|
||||
|
||||
if (!(await workflow.save())) {
|
||||
// Save was canceled, restore the previous workflow
|
||||
if (active !== workflow) {
|
||||
await active.load();
|
||||
await active.load()
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
} else if (res === "Cancel") {
|
||||
return;
|
||||
} else if (res === 'Cancel') {
|
||||
return
|
||||
}
|
||||
}
|
||||
workflow.changeTracker = null;
|
||||
this.openWorkflows.splice(this.openWorkflows.indexOf(workflow), 1);
|
||||
workflow.changeTracker = null
|
||||
this.openWorkflows.splice(this.openWorkflows.indexOf(workflow), 1)
|
||||
if (this.openWorkflows.length) {
|
||||
this.#activeWorkflow = this.openWorkflows[0];
|
||||
await this.#activeWorkflow.load();
|
||||
this.#activeWorkflow = this.openWorkflows[0]
|
||||
await this.#activeWorkflow.load()
|
||||
} else {
|
||||
// Load default
|
||||
await this.app.loadGraphData();
|
||||
await this.app.loadGraphData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ComfyWorkflow {
|
||||
#name;
|
||||
#path;
|
||||
#pathParts;
|
||||
#isFavorite = false;
|
||||
changeTracker: ChangeTracker | null = null;
|
||||
unsaved = false;
|
||||
manager: ComfyWorkflowManager;
|
||||
#name
|
||||
#path
|
||||
#pathParts
|
||||
#isFavorite = false
|
||||
changeTracker: ChangeTracker | null = null
|
||||
unsaved = false
|
||||
manager: ComfyWorkflowManager
|
||||
|
||||
get name() {
|
||||
return this.#name;
|
||||
return this.#name
|
||||
}
|
||||
|
||||
get path() {
|
||||
return this.#path;
|
||||
return this.#path
|
||||
}
|
||||
|
||||
get pathParts() {
|
||||
return this.#pathParts;
|
||||
return this.#pathParts
|
||||
}
|
||||
|
||||
get isFavorite() {
|
||||
return this.#isFavorite;
|
||||
return this.#isFavorite
|
||||
}
|
||||
|
||||
get isOpen() {
|
||||
return !!this.changeTracker;
|
||||
return !!this.changeTracker
|
||||
}
|
||||
|
||||
constructor(
|
||||
@@ -263,40 +263,40 @@ export class ComfyWorkflow {
|
||||
pathParts?: string[],
|
||||
isFavorite?: boolean
|
||||
) {
|
||||
this.manager = manager;
|
||||
this.manager = manager
|
||||
if (pathParts) {
|
||||
this.#updatePath(path, pathParts);
|
||||
this.#isFavorite = isFavorite;
|
||||
this.#updatePath(path, pathParts)
|
||||
this.#isFavorite = isFavorite
|
||||
} else {
|
||||
this.#name = path;
|
||||
this.unsaved = true;
|
||||
this.#name = path
|
||||
this.unsaved = true
|
||||
}
|
||||
}
|
||||
|
||||
#updatePath(path: string, pathParts: string[]) {
|
||||
this.#path = path;
|
||||
this.#path = path
|
||||
|
||||
if (!pathParts) {
|
||||
if (!path.includes("\\")) {
|
||||
pathParts = path.split("/");
|
||||
if (!path.includes('\\')) {
|
||||
pathParts = path.split('/')
|
||||
} else {
|
||||
pathParts = path.split("\\");
|
||||
pathParts = path.split('\\')
|
||||
}
|
||||
}
|
||||
|
||||
this.#pathParts = pathParts;
|
||||
this.#name = trimJsonExt(pathParts[pathParts.length - 1]);
|
||||
this.#pathParts = pathParts
|
||||
this.#name = trimJsonExt(pathParts[pathParts.length - 1])
|
||||
}
|
||||
|
||||
async getWorkflowData() {
|
||||
const resp = await api.getUserData("workflows/" + this.path);
|
||||
const resp = await api.getUserData('workflows/' + this.path)
|
||||
if (resp.status !== 200) {
|
||||
alert(
|
||||
`Error loading workflow file '${this.path}': ${resp.status} ${resp.statusText}`
|
||||
);
|
||||
return;
|
||||
)
|
||||
return
|
||||
}
|
||||
return await resp.json();
|
||||
return await resp.json()
|
||||
}
|
||||
|
||||
load = async () => {
|
||||
@@ -306,44 +306,44 @@ export class ComfyWorkflow {
|
||||
true,
|
||||
true,
|
||||
this
|
||||
);
|
||||
)
|
||||
} else {
|
||||
const data = await this.getWorkflowData();
|
||||
if (!data) return;
|
||||
await this.manager.app.loadGraphData(data, true, true, this);
|
||||
const data = await this.getWorkflowData()
|
||||
if (!data) return
|
||||
await this.manager.app.loadGraphData(data, true, true, this)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async save(saveAs = false) {
|
||||
if (!this.path || saveAs) {
|
||||
return !!(await this.#save(null, false));
|
||||
return !!(await this.#save(null, false))
|
||||
} else {
|
||||
return !!(await this.#save(this.path, true));
|
||||
return !!(await this.#save(this.path, true))
|
||||
}
|
||||
}
|
||||
|
||||
async favorite(value: boolean) {
|
||||
try {
|
||||
if (this.#isFavorite === value) return;
|
||||
this.#isFavorite = value;
|
||||
await this.manager.saveWorkflowMetadata();
|
||||
this.manager.dispatchEvent(new CustomEvent("favorite", { detail: this }));
|
||||
if (this.#isFavorite === value) return
|
||||
this.#isFavorite = value
|
||||
await this.manager.saveWorkflowMetadata()
|
||||
this.manager.dispatchEvent(new CustomEvent('favorite', { detail: this }))
|
||||
} catch (error) {
|
||||
alert(
|
||||
"Error favoriting workflow " +
|
||||
'Error favoriting workflow ' +
|
||||
this.path +
|
||||
"\n" +
|
||||
'\n' +
|
||||
(error.message ?? error)
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async rename(path: string) {
|
||||
path = appendJsonExt(path);
|
||||
path = appendJsonExt(path)
|
||||
let resp = await api.moveUserData(
|
||||
"workflows/" + this.path,
|
||||
"workflows/" + path
|
||||
);
|
||||
'workflows/' + this.path,
|
||||
'workflows/' + path
|
||||
)
|
||||
|
||||
if (resp.status === 409) {
|
||||
if (
|
||||
@@ -351,50 +351,50 @@ export class ComfyWorkflow {
|
||||
`Workflow '${path}' already exists, do you want to overwrite it?`
|
||||
)
|
||||
)
|
||||
return resp;
|
||||
return resp
|
||||
resp = await api.moveUserData(
|
||||
"workflows/" + this.path,
|
||||
"workflows/" + path,
|
||||
'workflows/' + this.path,
|
||||
'workflows/' + path,
|
||||
{ overwrite: true }
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
if (resp.status !== 200) {
|
||||
alert(
|
||||
`Error renaming workflow file '${this.path}': ${resp.status} ${resp.statusText}`
|
||||
);
|
||||
return;
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const isFav = this.isFavorite;
|
||||
const isFav = this.isFavorite
|
||||
if (isFav) {
|
||||
await this.favorite(false);
|
||||
await this.favorite(false)
|
||||
}
|
||||
path = (await resp.json()).substring("workflows/".length);
|
||||
this.#updatePath(path, null);
|
||||
path = (await resp.json()).substring('workflows/'.length)
|
||||
this.#updatePath(path, null)
|
||||
if (isFav) {
|
||||
await this.favorite(true);
|
||||
await this.favorite(true)
|
||||
}
|
||||
this.manager.dispatchEvent(new CustomEvent("rename", { detail: this }));
|
||||
setStorageValue("Comfy.PreviousWorkflow", this.path ?? "");
|
||||
this.manager.dispatchEvent(new CustomEvent('rename', { detail: this }))
|
||||
setStorageValue('Comfy.PreviousWorkflow', this.path ?? '')
|
||||
}
|
||||
|
||||
async insert() {
|
||||
const data = await this.getWorkflowData();
|
||||
if (!data) return;
|
||||
const data = await this.getWorkflowData()
|
||||
if (!data) return
|
||||
|
||||
const old = localStorage.getItem("litegrapheditor_clipboard");
|
||||
const old = localStorage.getItem('litegrapheditor_clipboard')
|
||||
// @ts-ignore
|
||||
const graph = new LGraph(data);
|
||||
const graph = new LGraph(data)
|
||||
const canvas = new LGraphCanvas(null, graph, {
|
||||
// @ts-ignore
|
||||
skip_events: true,
|
||||
skip_render: true,
|
||||
});
|
||||
canvas.selectNodes();
|
||||
canvas.copyToClipboard();
|
||||
this.manager.app.canvas.pasteFromClipboard();
|
||||
localStorage.setItem("litegrapheditor_clipboard", old);
|
||||
skip_render: true
|
||||
})
|
||||
canvas.selectNodes()
|
||||
canvas.copyToClipboard()
|
||||
this.manager.app.canvas.pasteFromClipboard()
|
||||
localStorage.setItem('litegrapheditor_clipboard', old)
|
||||
}
|
||||
|
||||
async delete() {
|
||||
@@ -402,84 +402,84 @@ export class ComfyWorkflow {
|
||||
|
||||
try {
|
||||
if (this.isFavorite) {
|
||||
await this.favorite(false);
|
||||
await this.favorite(false)
|
||||
}
|
||||
await api.deleteUserData("workflows/" + this.path);
|
||||
this.unsaved = true;
|
||||
this.#path = null;
|
||||
this.#pathParts = null;
|
||||
this.manager.workflows.splice(this.manager.workflows.indexOf(this), 1);
|
||||
this.manager.dispatchEvent(new CustomEvent("delete", { detail: this }));
|
||||
await api.deleteUserData('workflows/' + this.path)
|
||||
this.unsaved = true
|
||||
this.#path = null
|
||||
this.#pathParts = null
|
||||
this.manager.workflows.splice(this.manager.workflows.indexOf(this), 1)
|
||||
this.manager.dispatchEvent(new CustomEvent('delete', { detail: this }))
|
||||
} catch (error) {
|
||||
alert(`Error deleting workflow: ${error.message || error}`);
|
||||
alert(`Error deleting workflow: ${error.message || error}`)
|
||||
}
|
||||
}
|
||||
|
||||
track() {
|
||||
if (this.changeTracker) {
|
||||
this.changeTracker.restore();
|
||||
this.changeTracker.restore()
|
||||
} else {
|
||||
this.changeTracker = new ChangeTracker(this);
|
||||
this.changeTracker = new ChangeTracker(this)
|
||||
}
|
||||
}
|
||||
|
||||
async #save(path: string | null, overwrite: boolean) {
|
||||
if (!path) {
|
||||
path = prompt(
|
||||
"Save workflow as:",
|
||||
trimJsonExt(this.path) ?? this.name ?? "workflow"
|
||||
);
|
||||
if (!path) return;
|
||||
'Save workflow as:',
|
||||
trimJsonExt(this.path) ?? this.name ?? 'workflow'
|
||||
)
|
||||
if (!path) return
|
||||
}
|
||||
|
||||
path = appendJsonExt(path);
|
||||
path = appendJsonExt(path)
|
||||
|
||||
const p = await this.manager.app.graphToPrompt();
|
||||
const json = JSON.stringify(p.workflow, null, 2);
|
||||
let resp = await api.storeUserData("workflows/" + path, json, {
|
||||
const p = await this.manager.app.graphToPrompt()
|
||||
const json = JSON.stringify(p.workflow, null, 2)
|
||||
let resp = await api.storeUserData('workflows/' + path, json, {
|
||||
stringify: false,
|
||||
throwOnError: false,
|
||||
overwrite,
|
||||
});
|
||||
overwrite
|
||||
})
|
||||
if (resp.status === 409) {
|
||||
if (
|
||||
!confirm(
|
||||
`Workflow '${path}' already exists, do you want to overwrite it?`
|
||||
)
|
||||
)
|
||||
return;
|
||||
resp = await api.storeUserData("workflows/" + path, json, {
|
||||
stringify: false,
|
||||
});
|
||||
return
|
||||
resp = await api.storeUserData('workflows/' + path, json, {
|
||||
stringify: false
|
||||
})
|
||||
}
|
||||
|
||||
if (resp.status !== 200) {
|
||||
alert(
|
||||
`Error saving workflow '${this.path}': ${resp.status} ${resp.statusText}`
|
||||
);
|
||||
return;
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
path = (await resp.json()).substring("workflows/".length);
|
||||
path = (await resp.json()).substring('workflows/'.length)
|
||||
|
||||
if (!this.path) {
|
||||
// Saved new workflow, patch this instance
|
||||
this.#updatePath(path, null);
|
||||
await this.manager.loadWorkflows();
|
||||
this.unsaved = false;
|
||||
this.manager.dispatchEvent(new CustomEvent("rename", { detail: this }));
|
||||
setStorageValue("Comfy.PreviousWorkflow", this.path ?? "");
|
||||
this.#updatePath(path, null)
|
||||
await this.manager.loadWorkflows()
|
||||
this.unsaved = false
|
||||
this.manager.dispatchEvent(new CustomEvent('rename', { detail: this }))
|
||||
setStorageValue('Comfy.PreviousWorkflow', this.path ?? '')
|
||||
} else if (path !== this.path) {
|
||||
// Saved as, open the new copy
|
||||
await this.manager.loadWorkflows();
|
||||
const workflow = this.manager.workflowLookup[path];
|
||||
await workflow.load();
|
||||
await this.manager.loadWorkflows()
|
||||
const workflow = this.manager.workflowLookup[path]
|
||||
await workflow.load()
|
||||
} else {
|
||||
// Normal save
|
||||
this.unsaved = false;
|
||||
this.manager.dispatchEvent(new CustomEvent("save", { detail: this }));
|
||||
this.unsaved = false
|
||||
this.manager.dispatchEvent(new CustomEvent('save', { detail: this }))
|
||||
}
|
||||
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,191 +1,191 @@
|
||||
import { ComfyNodeDef } from "@/types/apiTypes";
|
||||
import { getNodeSource } from "@/types/nodeSource";
|
||||
import Fuse, { IFuseOptions, FuseSearchOptions } from "fuse.js";
|
||||
import _ from "lodash";
|
||||
import { ComfyNodeDef } from '@/types/apiTypes'
|
||||
import { getNodeSource } from '@/types/nodeSource'
|
||||
import Fuse, { IFuseOptions, FuseSearchOptions } from 'fuse.js'
|
||||
import _ from 'lodash'
|
||||
|
||||
export const SYSTEM_NODE_DEFS: ComfyNodeDef[] = [
|
||||
{
|
||||
name: "PrimitiveNode",
|
||||
display_name: "Primitive",
|
||||
category: "utils",
|
||||
name: 'PrimitiveNode',
|
||||
display_name: 'Primitive',
|
||||
category: 'utils',
|
||||
input: { required: {}, optional: {} },
|
||||
output: ["*"],
|
||||
output_name: ["connect to widget input"],
|
||||
output: ['*'],
|
||||
output_name: ['connect to widget input'],
|
||||
output_is_list: [false],
|
||||
python_module: "nodes",
|
||||
description: "Primitive values like numbers, strings, and booleans.",
|
||||
python_module: 'nodes',
|
||||
description: 'Primitive values like numbers, strings, and booleans.'
|
||||
},
|
||||
{
|
||||
name: "Reroute",
|
||||
display_name: "Reroute",
|
||||
category: "utils",
|
||||
input: { required: { "": ["*"] }, optional: {} },
|
||||
output: ["*"],
|
||||
output_name: [""],
|
||||
name: 'Reroute',
|
||||
display_name: 'Reroute',
|
||||
category: 'utils',
|
||||
input: { required: { '': ['*'] }, optional: {} },
|
||||
output: ['*'],
|
||||
output_name: [''],
|
||||
output_is_list: [false],
|
||||
python_module: "nodes",
|
||||
description: "Reroute the connection to another node.",
|
||||
python_module: 'nodes',
|
||||
description: 'Reroute the connection to another node.'
|
||||
},
|
||||
{
|
||||
name: "Note",
|
||||
display_name: "Note",
|
||||
category: "utils",
|
||||
name: 'Note',
|
||||
display_name: 'Note',
|
||||
category: 'utils',
|
||||
input: { required: {}, optional: {} },
|
||||
output: [],
|
||||
output_name: [],
|
||||
output_is_list: [],
|
||||
python_module: "nodes",
|
||||
description: "Node that add notes to your project",
|
||||
},
|
||||
];
|
||||
python_module: 'nodes',
|
||||
description: 'Node that add notes to your project'
|
||||
}
|
||||
]
|
||||
|
||||
export class FuseSearch<T> {
|
||||
private fuse: Fuse<T>;
|
||||
public readonly data: T[];
|
||||
private fuse: Fuse<T>
|
||||
public readonly data: T[]
|
||||
|
||||
constructor(
|
||||
data: T[],
|
||||
options?: IFuseOptions<T>,
|
||||
createIndex: boolean = true
|
||||
) {
|
||||
this.data = data;
|
||||
this.data = data
|
||||
const index =
|
||||
createIndex && options?.keys
|
||||
? Fuse.createIndex(options.keys, data)
|
||||
: undefined;
|
||||
this.fuse = new Fuse(data, options, index);
|
||||
: undefined
|
||||
this.fuse = new Fuse(data, options, index)
|
||||
}
|
||||
|
||||
public search(query: string, options?: FuseSearchOptions): T[] {
|
||||
if (!query || query === "") {
|
||||
return [...this.data];
|
||||
if (!query || query === '') {
|
||||
return [...this.data]
|
||||
}
|
||||
return this.fuse.search(query, options).map((result) => result.item);
|
||||
return this.fuse.search(query, options).map((result) => result.item)
|
||||
}
|
||||
}
|
||||
|
||||
export type FilterAndValue<T = string> = [NodeFilter<T>, T];
|
||||
export type FilterAndValue<T = string> = [NodeFilter<T>, T]
|
||||
|
||||
export abstract class NodeFilter<FilterOptionT = string> {
|
||||
public abstract readonly id: string;
|
||||
public abstract readonly name: string;
|
||||
public abstract readonly invokeSequence: string;
|
||||
public abstract readonly longInvokeSequence: string;
|
||||
public readonly fuseSearch: FuseSearch<FilterOptionT>;
|
||||
public abstract readonly id: string
|
||||
public abstract readonly name: string
|
||||
public abstract readonly invokeSequence: string
|
||||
public abstract readonly longInvokeSequence: string
|
||||
public readonly fuseSearch: FuseSearch<FilterOptionT>
|
||||
|
||||
constructor(nodeDefs: ComfyNodeDef[], options?: IFuseOptions<FilterOptionT>) {
|
||||
this.fuseSearch = new FuseSearch(this.getAllNodeOptions(nodeDefs), options);
|
||||
this.fuseSearch = new FuseSearch(this.getAllNodeOptions(nodeDefs), options)
|
||||
}
|
||||
|
||||
private getAllNodeOptions(nodeDefs: ComfyNodeDef[]): FilterOptionT[] {
|
||||
return [
|
||||
...new Set(
|
||||
nodeDefs.reduce((acc, nodeDef) => {
|
||||
return [...acc, ...this.getNodeOptions(nodeDef)];
|
||||
return [...acc, ...this.getNodeOptions(nodeDef)]
|
||||
}, [])
|
||||
),
|
||||
];
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
public abstract getNodeOptions(node: ComfyNodeDef): FilterOptionT[];
|
||||
public abstract getNodeOptions(node: ComfyNodeDef): FilterOptionT[]
|
||||
|
||||
public matches(node: ComfyNodeDef, value: FilterOptionT): boolean {
|
||||
return this.getNodeOptions(node).includes(value);
|
||||
return this.getNodeOptions(node).includes(value)
|
||||
}
|
||||
}
|
||||
|
||||
export class InputTypeFilter extends NodeFilter<string> {
|
||||
public readonly id: string = "input";
|
||||
public readonly name = "Input Type";
|
||||
public readonly invokeSequence = "i";
|
||||
public readonly longInvokeSequence = "input";
|
||||
public readonly id: string = 'input'
|
||||
public readonly name = 'Input Type'
|
||||
public readonly invokeSequence = 'i'
|
||||
public readonly longInvokeSequence = 'input'
|
||||
|
||||
public override getNodeOptions(node: ComfyNodeDef): string[] {
|
||||
const inputs = {
|
||||
...(node.input.required || {}),
|
||||
...(node.input.optional || {}),
|
||||
};
|
||||
...(node.input.optional || {})
|
||||
}
|
||||
return Object.values(inputs).map((input) => {
|
||||
const [inputType, inputSpec] = input;
|
||||
return typeof inputType === "string" ? inputType : "COMBO";
|
||||
});
|
||||
const [inputType, inputSpec] = input
|
||||
return typeof inputType === 'string' ? inputType : 'COMBO'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class OutputTypeFilter extends NodeFilter<string> {
|
||||
public readonly id: string = "output";
|
||||
public readonly name = "Output Type";
|
||||
public readonly invokeSequence = "o";
|
||||
public readonly longInvokeSequence = "output";
|
||||
public readonly id: string = 'output'
|
||||
public readonly name = 'Output Type'
|
||||
public readonly invokeSequence = 'o'
|
||||
public readonly longInvokeSequence = 'output'
|
||||
|
||||
public override getNodeOptions(node: ComfyNodeDef): string[] {
|
||||
const outputs = node.output || [];
|
||||
const outputs = node.output || []
|
||||
// "custom_nodes.was-node-suite-comfyui"
|
||||
// has a custom node with an output that is not an array.
|
||||
// https://github.com/WASasquatch/was-node-suite-comfyui/pull/440
|
||||
if (!(outputs instanceof Array)) {
|
||||
console.error("Invalid output type", node);
|
||||
return [];
|
||||
console.error('Invalid output type', node)
|
||||
return []
|
||||
}
|
||||
return outputs.map((output) => {
|
||||
return typeof output === "string" ? output : output[0];
|
||||
});
|
||||
return typeof output === 'string' ? output : output[0]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeSourceFilter extends NodeFilter<string> {
|
||||
public readonly id: string = "source";
|
||||
public readonly name = "Source";
|
||||
public readonly invokeSequence = "s";
|
||||
public readonly longInvokeSequence = "source";
|
||||
public readonly id: string = 'source'
|
||||
public readonly name = 'Source'
|
||||
public readonly invokeSequence = 's'
|
||||
public readonly longInvokeSequence = 'source'
|
||||
|
||||
public override getNodeOptions(node: ComfyNodeDef): string[] {
|
||||
return [getNodeSource(node.python_module).displayText];
|
||||
return [getNodeSource(node.python_module).displayText]
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeCategoryFilter extends NodeFilter<string> {
|
||||
public readonly id: string = "category";
|
||||
public readonly name = "Category";
|
||||
public readonly invokeSequence = "c";
|
||||
public readonly longInvokeSequence = "category";
|
||||
public readonly id: string = 'category'
|
||||
public readonly name = 'Category'
|
||||
public readonly invokeSequence = 'c'
|
||||
public readonly longInvokeSequence = 'category'
|
||||
|
||||
public override getNodeOptions(node: ComfyNodeDef): string[] {
|
||||
return [node.category];
|
||||
return [node.category]
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeSearchService {
|
||||
public readonly nodeFuseSearch: FuseSearch<ComfyNodeDef>;
|
||||
public readonly nodeFilters: NodeFilter<string>[];
|
||||
public readonly nodeFuseSearch: FuseSearch<ComfyNodeDef>
|
||||
public readonly nodeFilters: NodeFilter<string>[]
|
||||
|
||||
constructor(data: ComfyNodeDef[]) {
|
||||
this.nodeFuseSearch = new FuseSearch(data, {
|
||||
keys: ["name", "display_name", "description"],
|
||||
keys: ['name', 'display_name', 'description'],
|
||||
includeScore: true,
|
||||
threshold: 0.6,
|
||||
shouldSort: true,
|
||||
});
|
||||
shouldSort: true
|
||||
})
|
||||
|
||||
const filterSearchOptions = {
|
||||
includeScore: true,
|
||||
threshold: 0.6,
|
||||
shouldSort: true,
|
||||
};
|
||||
shouldSort: true
|
||||
}
|
||||
|
||||
this.nodeFilters = [
|
||||
new InputTypeFilter(data, filterSearchOptions),
|
||||
new OutputTypeFilter(data, filterSearchOptions),
|
||||
new NodeCategoryFilter(data, filterSearchOptions),
|
||||
];
|
||||
new NodeCategoryFilter(data, filterSearchOptions)
|
||||
]
|
||||
|
||||
if (data[0].python_module !== undefined) {
|
||||
this.nodeFilters.push(new NodeSourceFilter(data, filterSearchOptions));
|
||||
this.nodeFilters.push(new NodeSourceFilter(data, filterSearchOptions))
|
||||
}
|
||||
}
|
||||
|
||||
public endsWithFilterStartSequence(query: string): boolean {
|
||||
return query.endsWith(":");
|
||||
return query.endsWith(':')
|
||||
}
|
||||
|
||||
public searchNode(
|
||||
@@ -193,19 +193,19 @@ export class NodeSearchService {
|
||||
filters: FilterAndValue<string>[] = [],
|
||||
options?: FuseSearchOptions
|
||||
): ComfyNodeDef[] {
|
||||
const matchedNodes = this.nodeFuseSearch.search(query);
|
||||
const matchedNodes = this.nodeFuseSearch.search(query)
|
||||
|
||||
const results = matchedNodes.filter((node) => {
|
||||
return _.every(filters, (filterAndValue) => {
|
||||
const [filter, value] = filterAndValue;
|
||||
return filter.matches(node, value);
|
||||
});
|
||||
});
|
||||
const [filter, value] = filterAndValue
|
||||
return filter.matches(node, value)
|
||||
})
|
||||
})
|
||||
|
||||
return options?.limit ? results.slice(0, options.limit) : results;
|
||||
return options?.limit ? results.slice(0, options.limit) : results
|
||||
}
|
||||
|
||||
public getFilterById(id: string): NodeFilter<string> | undefined {
|
||||
return this.nodeFilters.find((filter) => filter.id === id);
|
||||
return this.nodeFilters.find((filter) => filter.id === id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +1,72 @@
|
||||
import { NodeSearchService } from "@/services/nodeSearchService";
|
||||
import { ComfyNodeDef } from "@/types/apiTypes";
|
||||
import { defineStore } from "pinia";
|
||||
import { NodeSearchService } from '@/services/nodeSearchService'
|
||||
import { ComfyNodeDef } from '@/types/apiTypes'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const SYSTEM_NODE_DEFS: ComfyNodeDef[] = [
|
||||
{
|
||||
name: "PrimitiveNode",
|
||||
display_name: "Primitive",
|
||||
category: "utils",
|
||||
name: 'PrimitiveNode',
|
||||
display_name: 'Primitive',
|
||||
category: 'utils',
|
||||
input: { required: {}, optional: {} },
|
||||
output: ["*"],
|
||||
output_name: ["connect to widget input"],
|
||||
output: ['*'],
|
||||
output_name: ['connect to widget input'],
|
||||
output_is_list: [false],
|
||||
python_module: "nodes",
|
||||
description: "Primitive values like numbers, strings, and booleans.",
|
||||
python_module: 'nodes',
|
||||
description: 'Primitive values like numbers, strings, and booleans.'
|
||||
},
|
||||
{
|
||||
name: "Reroute",
|
||||
display_name: "Reroute",
|
||||
category: "utils",
|
||||
input: { required: { "": ["*"] }, optional: {} },
|
||||
output: ["*"],
|
||||
output_name: [""],
|
||||
name: 'Reroute',
|
||||
display_name: 'Reroute',
|
||||
category: 'utils',
|
||||
input: { required: { '': ['*'] }, optional: {} },
|
||||
output: ['*'],
|
||||
output_name: [''],
|
||||
output_is_list: [false],
|
||||
python_module: "nodes",
|
||||
description: "Reroute the connection to another node.",
|
||||
python_module: 'nodes',
|
||||
description: 'Reroute the connection to another node.'
|
||||
},
|
||||
{
|
||||
name: "Note",
|
||||
display_name: "Note",
|
||||
category: "utils",
|
||||
name: 'Note',
|
||||
display_name: 'Note',
|
||||
category: 'utils',
|
||||
input: { required: {}, optional: {} },
|
||||
output: [],
|
||||
output_name: [],
|
||||
output_is_list: [],
|
||||
python_module: "nodes",
|
||||
description: "Node that add notes to your project",
|
||||
},
|
||||
];
|
||||
python_module: 'nodes',
|
||||
description: 'Node that add notes to your project'
|
||||
}
|
||||
]
|
||||
|
||||
const SYSTEM_NODE_DEFS_BY_NAME = SYSTEM_NODE_DEFS.reduce((acc, nodeDef) => {
|
||||
acc[nodeDef.name] = nodeDef;
|
||||
return acc;
|
||||
}, {}) as Record<string, ComfyNodeDef>;
|
||||
acc[nodeDef.name] = nodeDef
|
||||
return acc
|
||||
}, {}) as Record<string, ComfyNodeDef>
|
||||
|
||||
interface State {
|
||||
nodeDefsByName: Record<string, ComfyNodeDef>;
|
||||
nodeDefsByName: Record<string, ComfyNodeDef>
|
||||
}
|
||||
|
||||
export const useNodeDefStore = defineStore("nodeDef", {
|
||||
export const useNodeDefStore = defineStore('nodeDef', {
|
||||
state: (): State => ({
|
||||
nodeDefsByName: SYSTEM_NODE_DEFS_BY_NAME,
|
||||
nodeDefsByName: SYSTEM_NODE_DEFS_BY_NAME
|
||||
}),
|
||||
getters: {
|
||||
nodeDefs(state) {
|
||||
return Object.values(state.nodeDefsByName);
|
||||
return Object.values(state.nodeDefsByName)
|
||||
},
|
||||
nodeSearchService(state) {
|
||||
return new NodeSearchService(Object.values(state.nodeDefsByName));
|
||||
},
|
||||
return new NodeSearchService(Object.values(state.nodeDefsByName))
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
addNodeDef(nodeDef: ComfyNodeDef) {
|
||||
this.nodeDefsByName[nodeDef.name] = nodeDef;
|
||||
this.nodeDefsByName[nodeDef.name] = nodeDef
|
||||
},
|
||||
addNodeDefs(nodeDefs: ComfyNodeDef[]) {
|
||||
for (const nodeDef of nodeDefs) {
|
||||
this.nodeDefsByName[nodeDef.name] = nodeDef;
|
||||
this.nodeDefsByName[nodeDef.name] = nodeDef
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,184 +1,184 @@
|
||||
import { api } from "@/scripts/api";
|
||||
import { app } from "@/scripts/app";
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import {
|
||||
validateTaskItem,
|
||||
TaskItem,
|
||||
TaskType,
|
||||
TaskPrompt,
|
||||
TaskStatus,
|
||||
TaskOutput,
|
||||
} from "@/types/apiTypes";
|
||||
import { plainToClass } from "class-transformer";
|
||||
import _ from "lodash";
|
||||
import { defineStore } from "pinia";
|
||||
import { toRaw } from "vue";
|
||||
TaskOutput
|
||||
} from '@/types/apiTypes'
|
||||
import { plainToClass } from 'class-transformer'
|
||||
import _ from 'lodash'
|
||||
import { defineStore } from 'pinia'
|
||||
import { toRaw } from 'vue'
|
||||
|
||||
// Task type used in the API.
|
||||
export type APITaskType = "queue" | "history";
|
||||
export type APITaskType = 'queue' | 'history'
|
||||
|
||||
export enum TaskItemDisplayStatus {
|
||||
Running = "Running",
|
||||
Pending = "Pending",
|
||||
Completed = "Completed",
|
||||
Failed = "Failed",
|
||||
Cancelled = "Cancelled",
|
||||
Running = 'Running',
|
||||
Pending = 'Pending',
|
||||
Completed = 'Completed',
|
||||
Failed = 'Failed',
|
||||
Cancelled = 'Cancelled'
|
||||
}
|
||||
|
||||
export class TaskItemImpl {
|
||||
taskType: TaskType;
|
||||
prompt: TaskPrompt;
|
||||
status?: TaskStatus;
|
||||
outputs?: TaskOutput;
|
||||
taskType: TaskType
|
||||
prompt: TaskPrompt
|
||||
status?: TaskStatus
|
||||
outputs?: TaskOutput
|
||||
|
||||
get apiTaskType(): APITaskType {
|
||||
switch (this.taskType) {
|
||||
case "Running":
|
||||
case "Pending":
|
||||
return "queue";
|
||||
case "History":
|
||||
return "history";
|
||||
case 'Running':
|
||||
case 'Pending':
|
||||
return 'queue'
|
||||
case 'History':
|
||||
return 'history'
|
||||
}
|
||||
}
|
||||
|
||||
get queueIndex() {
|
||||
return this.prompt[0];
|
||||
return this.prompt[0]
|
||||
}
|
||||
|
||||
get promptId() {
|
||||
return this.prompt[1];
|
||||
return this.prompt[1]
|
||||
}
|
||||
|
||||
get promptInputs() {
|
||||
return this.prompt[2];
|
||||
return this.prompt[2]
|
||||
}
|
||||
|
||||
get extraData() {
|
||||
return this.prompt[3];
|
||||
return this.prompt[3]
|
||||
}
|
||||
|
||||
get outputsToExecute() {
|
||||
return this.prompt[4];
|
||||
return this.prompt[4]
|
||||
}
|
||||
|
||||
get extraPngInfo() {
|
||||
return this.extraData.extra_pnginfo;
|
||||
return this.extraData.extra_pnginfo
|
||||
}
|
||||
|
||||
get clientId() {
|
||||
return this.extraData.client_id;
|
||||
return this.extraData.client_id
|
||||
}
|
||||
|
||||
get workflow() {
|
||||
return this.extraPngInfo.workflow;
|
||||
return this.extraPngInfo.workflow
|
||||
}
|
||||
|
||||
get messages() {
|
||||
return this.status?.messages || [];
|
||||
return this.status?.messages || []
|
||||
}
|
||||
|
||||
get interrupted() {
|
||||
return _.some(
|
||||
this.messages,
|
||||
(message) => message[0] === "execution_interrupted"
|
||||
);
|
||||
(message) => message[0] === 'execution_interrupted'
|
||||
)
|
||||
}
|
||||
|
||||
get isHistory() {
|
||||
return this.taskType === "History";
|
||||
return this.taskType === 'History'
|
||||
}
|
||||
|
||||
get isRunning() {
|
||||
return this.taskType === "Running";
|
||||
return this.taskType === 'Running'
|
||||
}
|
||||
|
||||
get displayStatus(): TaskItemDisplayStatus {
|
||||
switch (this.taskType) {
|
||||
case "Running":
|
||||
return TaskItemDisplayStatus.Running;
|
||||
case "Pending":
|
||||
return TaskItemDisplayStatus.Pending;
|
||||
case "History":
|
||||
case 'Running':
|
||||
return TaskItemDisplayStatus.Running
|
||||
case 'Pending':
|
||||
return TaskItemDisplayStatus.Pending
|
||||
case 'History':
|
||||
switch (this.status!.status_str) {
|
||||
case "success":
|
||||
return TaskItemDisplayStatus.Completed;
|
||||
case "error":
|
||||
case 'success':
|
||||
return TaskItemDisplayStatus.Completed
|
||||
case 'error':
|
||||
return this.interrupted
|
||||
? TaskItemDisplayStatus.Cancelled
|
||||
: TaskItemDisplayStatus.Failed;
|
||||
: TaskItemDisplayStatus.Failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get executionStartTimestamp() {
|
||||
const message = this.messages.find(
|
||||
(message) => message[0] === "execution_start"
|
||||
);
|
||||
return message ? message[1].timestamp : undefined;
|
||||
(message) => message[0] === 'execution_start'
|
||||
)
|
||||
return message ? message[1].timestamp : undefined
|
||||
}
|
||||
|
||||
get executionEndTimestamp() {
|
||||
const messages = this.messages.filter((message) =>
|
||||
[
|
||||
"execution_success",
|
||||
"execution_interrupted",
|
||||
"execution_error",
|
||||
'execution_success',
|
||||
'execution_interrupted',
|
||||
'execution_error'
|
||||
].includes(message[0])
|
||||
);
|
||||
)
|
||||
if (!messages.length) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
return _.max(messages.map((message) => message[1].timestamp));
|
||||
return _.max(messages.map((message) => message[1].timestamp))
|
||||
}
|
||||
|
||||
get executionTime() {
|
||||
if (!this.executionStartTimestamp || !this.executionEndTimestamp) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
return this.executionEndTimestamp - this.executionStartTimestamp;
|
||||
return this.executionEndTimestamp - this.executionStartTimestamp
|
||||
}
|
||||
|
||||
get executionTimeInSeconds() {
|
||||
return this.executionTime !== undefined
|
||||
? this.executionTime / 1000
|
||||
: undefined;
|
||||
: undefined
|
||||
}
|
||||
|
||||
public async loadWorkflow() {
|
||||
await app.loadGraphData(toRaw(this.workflow));
|
||||
await app.loadGraphData(toRaw(this.workflow))
|
||||
if (this.outputs) {
|
||||
app.nodeOutputs = toRaw(this.outputs);
|
||||
app.nodeOutputs = toRaw(this.outputs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface State {
|
||||
runningTasks: TaskItemImpl[];
|
||||
pendingTasks: TaskItemImpl[];
|
||||
historyTasks: TaskItemImpl[];
|
||||
runningTasks: TaskItemImpl[]
|
||||
pendingTasks: TaskItemImpl[]
|
||||
historyTasks: TaskItemImpl[]
|
||||
}
|
||||
|
||||
export const useQueueStore = defineStore("queue", {
|
||||
export const useQueueStore = defineStore('queue', {
|
||||
state: (): State => ({
|
||||
runningTasks: [],
|
||||
pendingTasks: [],
|
||||
historyTasks: [],
|
||||
historyTasks: []
|
||||
}),
|
||||
getters: {
|
||||
tasks(state) {
|
||||
return [
|
||||
...state.pendingTasks,
|
||||
...state.runningTasks,
|
||||
...state.historyTasks,
|
||||
];
|
||||
},
|
||||
...state.historyTasks
|
||||
]
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
// Fetch the queue data from the API
|
||||
async update() {
|
||||
const [queue, history] = await Promise.all([
|
||||
api.getQueue(),
|
||||
api.getHistory(),
|
||||
]);
|
||||
api.getHistory()
|
||||
])
|
||||
|
||||
const toClassAll = (tasks: TaskItem[]): TaskItemImpl[] =>
|
||||
tasks
|
||||
@@ -186,21 +186,21 @@ export const useQueueStore = defineStore("queue", {
|
||||
.filter((result) => result.success)
|
||||
.map((result) => plainToClass(TaskItemImpl, result.data))
|
||||
// Desc order to show the latest tasks first
|
||||
.sort((a, b) => b.queueIndex - a.queueIndex);
|
||||
.sort((a, b) => b.queueIndex - a.queueIndex)
|
||||
|
||||
this.runningTasks = toClassAll(queue.Running);
|
||||
this.pendingTasks = toClassAll(queue.Pending);
|
||||
this.historyTasks = toClassAll(history.History);
|
||||
this.runningTasks = toClassAll(queue.Running)
|
||||
this.pendingTasks = toClassAll(queue.Pending)
|
||||
this.historyTasks = toClassAll(history.History)
|
||||
},
|
||||
async clear() {
|
||||
await Promise.all(
|
||||
["queue", "history"].map((type) => api.clearItems(type))
|
||||
);
|
||||
await this.update();
|
||||
['queue', 'history'].map((type) => api.clearItems(type))
|
||||
)
|
||||
await this.update()
|
||||
},
|
||||
async delete(task: TaskItemImpl) {
|
||||
await api.deleteItem(task.apiTaskType, task.promptId);
|
||||
await this.update();
|
||||
},
|
||||
},
|
||||
});
|
||||
await api.deleteItem(task.apiTaskType, task.promptId)
|
||||
await this.update()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -7,35 +7,35 @@
|
||||
* settings directly updates the settingStore.settingValues.
|
||||
*/
|
||||
|
||||
import { app } from "@/scripts/app";
|
||||
import { ComfySettingsDialog } from "@/scripts/ui/settings";
|
||||
import { defineStore } from "pinia";
|
||||
import { app } from '@/scripts/app'
|
||||
import { ComfySettingsDialog } from '@/scripts/ui/settings'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
interface State {
|
||||
settingValues: Record<string, any>;
|
||||
settingValues: Record<string, any>
|
||||
}
|
||||
|
||||
export const useSettingStore = defineStore("setting", {
|
||||
export const useSettingStore = defineStore('setting', {
|
||||
state: (): State => ({
|
||||
settingValues: {},
|
||||
settingValues: {}
|
||||
}),
|
||||
actions: {
|
||||
addSettings(settings: ComfySettingsDialog) {
|
||||
for (const id in settings.settingsLookup) {
|
||||
const value = settings.getSettingValue(id);
|
||||
this.settingValues[id] = value;
|
||||
const value = settings.getSettingValue(id)
|
||||
this.settingValues[id] = value
|
||||
}
|
||||
},
|
||||
|
||||
set(key: string, value: any) {
|
||||
this.settingValues[key] = value;
|
||||
app.ui.settings.setSettingValue(key, value);
|
||||
this.settingValues[key] = value
|
||||
app.ui.settings.setSettingValue(key, value)
|
||||
},
|
||||
|
||||
get(key: string) {
|
||||
return (
|
||||
this.settingValues[key] ?? app.ui.settings.getSettingDefaultValue(key)
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
interface WorkspaceState {
|
||||
activeSidebarTab: string | null;
|
||||
sidebarTabsOrder: string[]; // Array of tab IDs in order
|
||||
activeSidebarTab: string | null
|
||||
sidebarTabsOrder: string[] // Array of tab IDs in order
|
||||
}
|
||||
|
||||
export const useWorkspaceStore = defineStore("workspace", {
|
||||
export const useWorkspaceStore = defineStore('workspace', {
|
||||
state: (): WorkspaceState => ({
|
||||
activeSidebarTab: null,
|
||||
sidebarTabsOrder: [],
|
||||
sidebarTabsOrder: []
|
||||
}),
|
||||
actions: {
|
||||
updateActiveSidebarTab(tabId: string) {
|
||||
this.activeSidebarTab = tabId;
|
||||
this.activeSidebarTab = tabId
|
||||
},
|
||||
updateSidebarOrder(newOrder: string[]) {
|
||||
this.sidebarTabsOrder = newOrder;
|
||||
this.sidebarTabsOrder = newOrder
|
||||
},
|
||||
serialize() {
|
||||
return JSON.stringify({
|
||||
activeSidebarTab: this.activeSidebarTab,
|
||||
sidebarTabsOrder: this.sidebarTabsOrder,
|
||||
});
|
||||
sidebarTabsOrder: this.sidebarTabsOrder
|
||||
})
|
||||
},
|
||||
deserialize(state: string) {
|
||||
const parsedState = JSON.parse(state);
|
||||
this.sidebarTabsOrder = parsedState.sidebarTabsOrder;
|
||||
this.activeSidebarTab = parsedState.activeSidebarTab;
|
||||
},
|
||||
},
|
||||
});
|
||||
const parsedState = JSON.parse(state)
|
||||
this.sidebarTabsOrder = parsedState.sidebarTabsOrder
|
||||
this.activeSidebarTab = parsedState.activeSidebarTab
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,65 +1,65 @@
|
||||
import { ZodType, z } from "zod";
|
||||
import { zComfyWorkflow } from "./comfyWorkflow";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { ZodType, z } from 'zod'
|
||||
import { zComfyWorkflow } from './comfyWorkflow'
|
||||
import { fromZodError } from 'zod-validation-error'
|
||||
|
||||
const zNodeId = z.union([z.number(), z.string()]);
|
||||
const zNodeType = z.string();
|
||||
const zQueueIndex = z.number();
|
||||
const zPromptId = z.string();
|
||||
const zNodeId = z.union([z.number(), z.string()])
|
||||
const zNodeType = z.string()
|
||||
const zQueueIndex = z.number()
|
||||
const zPromptId = z.string()
|
||||
|
||||
const zPromptInputItem = z.object({
|
||||
inputs: z.record(z.string(), z.any()),
|
||||
class_type: zNodeType,
|
||||
});
|
||||
class_type: zNodeType
|
||||
})
|
||||
|
||||
const zPromptInputs = z.record(zPromptInputItem);
|
||||
const zPromptInputs = z.record(zPromptInputItem)
|
||||
|
||||
const zExtraPngInfo = z
|
||||
.object({
|
||||
workflow: zComfyWorkflow,
|
||||
workflow: zComfyWorkflow
|
||||
})
|
||||
.passthrough();
|
||||
.passthrough()
|
||||
|
||||
const zExtraData = z.object({
|
||||
extra_pnginfo: zExtraPngInfo,
|
||||
client_id: z.string(),
|
||||
});
|
||||
const zOutputsToExecute = z.array(zNodeId);
|
||||
client_id: z.string()
|
||||
})
|
||||
const zOutputsToExecute = z.array(zNodeId)
|
||||
|
||||
const zMessageDetailBase = z.object({
|
||||
prompt_id: zPromptId,
|
||||
timestamp: z.number(),
|
||||
});
|
||||
timestamp: z.number()
|
||||
})
|
||||
|
||||
const zExecutionStartMessage = z.tuple([
|
||||
z.literal("execution_start"),
|
||||
zMessageDetailBase,
|
||||
]);
|
||||
z.literal('execution_start'),
|
||||
zMessageDetailBase
|
||||
])
|
||||
|
||||
const zExecutionSuccessMessage = z.tuple([
|
||||
z.literal("execution_success"),
|
||||
zMessageDetailBase,
|
||||
]);
|
||||
z.literal('execution_success'),
|
||||
zMessageDetailBase
|
||||
])
|
||||
|
||||
const zExecutionCachedMessage = z.tuple([
|
||||
z.literal("execution_cached"),
|
||||
z.literal('execution_cached'),
|
||||
zMessageDetailBase.extend({
|
||||
nodes: z.array(zNodeId),
|
||||
}),
|
||||
]);
|
||||
nodes: z.array(zNodeId)
|
||||
})
|
||||
])
|
||||
|
||||
const zExecutionInterruptedMessage = z.tuple([
|
||||
z.literal("execution_interrupted"),
|
||||
z.literal('execution_interrupted'),
|
||||
zMessageDetailBase.extend({
|
||||
// InterruptProcessingException
|
||||
node_id: zNodeId,
|
||||
node_type: zNodeType,
|
||||
executed: z.array(zNodeId),
|
||||
}),
|
||||
]);
|
||||
executed: z.array(zNodeId)
|
||||
})
|
||||
])
|
||||
|
||||
const zExecutionErrorMessage = z.tuple([
|
||||
z.literal("execution_error"),
|
||||
z.literal('execution_error'),
|
||||
zMessageDetailBase.extend({
|
||||
node_id: zNodeId,
|
||||
node_type: zNodeType,
|
||||
@@ -69,154 +69,154 @@ const zExecutionErrorMessage = z.tuple([
|
||||
exception_type: z.string(),
|
||||
traceback: z.string(),
|
||||
current_inputs: z.any(),
|
||||
current_outputs: z.any(),
|
||||
}),
|
||||
]);
|
||||
current_outputs: z.any()
|
||||
})
|
||||
])
|
||||
|
||||
const zStatusMessage = z.union([
|
||||
zExecutionStartMessage,
|
||||
zExecutionSuccessMessage,
|
||||
zExecutionCachedMessage,
|
||||
zExecutionInterruptedMessage,
|
||||
zExecutionErrorMessage,
|
||||
]);
|
||||
zExecutionErrorMessage
|
||||
])
|
||||
|
||||
const zStatus = z.object({
|
||||
status_str: z.enum(["success", "error"]),
|
||||
status_str: z.enum(['success', 'error']),
|
||||
completed: z.boolean(),
|
||||
messages: z.array(zStatusMessage),
|
||||
});
|
||||
messages: z.array(zStatusMessage)
|
||||
})
|
||||
|
||||
// TODO: this is a placeholder
|
||||
const zOutput = z.any();
|
||||
const zOutput = z.any()
|
||||
|
||||
const zTaskPrompt = z.tuple([
|
||||
zQueueIndex,
|
||||
zPromptId,
|
||||
zPromptInputs,
|
||||
zExtraData,
|
||||
zOutputsToExecute,
|
||||
]);
|
||||
zOutputsToExecute
|
||||
])
|
||||
|
||||
const zRunningTaskItem = z.object({
|
||||
taskType: z.literal("Running"),
|
||||
taskType: z.literal('Running'),
|
||||
prompt: zTaskPrompt,
|
||||
// @Deprecated
|
||||
remove: z.object({
|
||||
name: z.literal("Cancel"),
|
||||
cb: z.function(),
|
||||
}),
|
||||
});
|
||||
name: z.literal('Cancel'),
|
||||
cb: z.function()
|
||||
})
|
||||
})
|
||||
|
||||
const zPendingTaskItem = z.object({
|
||||
taskType: z.literal("Pending"),
|
||||
prompt: zTaskPrompt,
|
||||
});
|
||||
taskType: z.literal('Pending'),
|
||||
prompt: zTaskPrompt
|
||||
})
|
||||
|
||||
const zTaskOutput = z.record(zNodeId, zOutput);
|
||||
const zTaskOutput = z.record(zNodeId, zOutput)
|
||||
|
||||
const zHistoryTaskItem = z.object({
|
||||
taskType: z.literal("History"),
|
||||
taskType: z.literal('History'),
|
||||
prompt: zTaskPrompt,
|
||||
status: zStatus.optional(),
|
||||
outputs: zTaskOutput,
|
||||
});
|
||||
outputs: zTaskOutput
|
||||
})
|
||||
|
||||
const zTaskItem = z.union([
|
||||
zRunningTaskItem,
|
||||
zPendingTaskItem,
|
||||
zHistoryTaskItem,
|
||||
]);
|
||||
zHistoryTaskItem
|
||||
])
|
||||
|
||||
const zTaskType = z.union([
|
||||
z.literal("Running"),
|
||||
z.literal("Pending"),
|
||||
z.literal("History"),
|
||||
]);
|
||||
z.literal('Running'),
|
||||
z.literal('Pending'),
|
||||
z.literal('History')
|
||||
])
|
||||
|
||||
export type TaskType = z.infer<typeof zTaskType>;
|
||||
export type TaskPrompt = z.infer<typeof zTaskPrompt>;
|
||||
export type TaskStatus = z.infer<typeof zStatus>;
|
||||
export type TaskOutput = z.infer<typeof zTaskOutput>;
|
||||
export type TaskType = z.infer<typeof zTaskType>
|
||||
export type TaskPrompt = z.infer<typeof zTaskPrompt>
|
||||
export type TaskStatus = z.infer<typeof zStatus>
|
||||
export type TaskOutput = z.infer<typeof zTaskOutput>
|
||||
|
||||
// `/queue`
|
||||
export type RunningTaskItem = z.infer<typeof zRunningTaskItem>;
|
||||
export type PendingTaskItem = z.infer<typeof zPendingTaskItem>;
|
||||
export type RunningTaskItem = z.infer<typeof zRunningTaskItem>
|
||||
export type PendingTaskItem = z.infer<typeof zPendingTaskItem>
|
||||
// `/history`
|
||||
export type HistoryTaskItem = z.infer<typeof zHistoryTaskItem>;
|
||||
export type TaskItem = z.infer<typeof zTaskItem>;
|
||||
export type HistoryTaskItem = z.infer<typeof zHistoryTaskItem>
|
||||
export type TaskItem = z.infer<typeof zTaskItem>
|
||||
|
||||
export function validateTaskItem(taskItem: unknown) {
|
||||
const result = zTaskItem.safeParse(taskItem);
|
||||
const result = zTaskItem.safeParse(taskItem)
|
||||
if (!result.success) {
|
||||
const zodError = fromZodError(result.error);
|
||||
const zodError = fromZodError(result.error)
|
||||
// TODO accept a callback to report error.
|
||||
console.warn(
|
||||
`Invalid TaskItem: ${JSON.stringify(taskItem)}\n${zodError.message}`
|
||||
);
|
||||
)
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
|
||||
function inputSpec(
|
||||
spec: [ZodType, ZodType],
|
||||
allowUpcast: boolean = true
|
||||
): ZodType {
|
||||
const [inputType, inputSpec] = spec;
|
||||
const [inputType, inputSpec] = spec
|
||||
// e.g. "INT" => ["INT", {}]
|
||||
const upcastTypes: ZodType[] = allowUpcast
|
||||
? [inputType.transform((type) => [type, {}])]
|
||||
: [];
|
||||
: []
|
||||
|
||||
return z.union([
|
||||
z.tuple([inputType, inputSpec]),
|
||||
z.tuple([inputType]).transform(([type]) => [type, {}]),
|
||||
...upcastTypes,
|
||||
]);
|
||||
...upcastTypes
|
||||
])
|
||||
}
|
||||
|
||||
const zIntInputSpec = inputSpec([
|
||||
z.literal("INT"),
|
||||
z.literal('INT'),
|
||||
z.object({
|
||||
min: z.number().optional(),
|
||||
max: z.number().optional(),
|
||||
step: z.number().optional(),
|
||||
default: z.number().optional(),
|
||||
forceInput: z.boolean().optional(),
|
||||
}),
|
||||
]);
|
||||
forceInput: z.boolean().optional()
|
||||
})
|
||||
])
|
||||
|
||||
const zFloatInputSpec = inputSpec([
|
||||
z.literal("FLOAT"),
|
||||
z.literal('FLOAT'),
|
||||
z.object({
|
||||
min: z.number().optional(),
|
||||
max: z.number().optional(),
|
||||
step: z.number().optional(),
|
||||
round: z.number().optional(),
|
||||
default: z.number().optional(),
|
||||
forceInput: z.boolean().optional(),
|
||||
}),
|
||||
]);
|
||||
forceInput: z.boolean().optional()
|
||||
})
|
||||
])
|
||||
|
||||
const zBooleanInputSpec = inputSpec([
|
||||
z.literal("BOOLEAN"),
|
||||
z.literal('BOOLEAN'),
|
||||
z.object({
|
||||
label_on: z.string().optional(),
|
||||
label_off: z.string().optional(),
|
||||
default: z.boolean().optional(),
|
||||
forceInput: z.boolean().optional(),
|
||||
}),
|
||||
]);
|
||||
forceInput: z.boolean().optional()
|
||||
})
|
||||
])
|
||||
|
||||
const zStringInputSpec = inputSpec([
|
||||
z.literal("STRING"),
|
||||
z.literal('STRING'),
|
||||
z.object({
|
||||
default: z.string().optional(),
|
||||
multiline: z.boolean().optional(),
|
||||
dynamicPrompts: z.boolean().optional(),
|
||||
forceInput: z.boolean().optional(),
|
||||
}),
|
||||
]);
|
||||
forceInput: z.boolean().optional()
|
||||
})
|
||||
])
|
||||
|
||||
// Dropdown Selection.
|
||||
const zComboInputSpec = inputSpec(
|
||||
@@ -226,19 +226,19 @@ const zComboInputSpec = inputSpec(
|
||||
default: z.any().optional(),
|
||||
control_after_generate: z.boolean().optional(),
|
||||
image_upload: z.boolean().optional(),
|
||||
forceInput: z.boolean().optional(),
|
||||
}),
|
||||
forceInput: z.boolean().optional()
|
||||
})
|
||||
],
|
||||
/* allowUpcast=*/ false
|
||||
);
|
||||
)
|
||||
|
||||
const zCustomInputSpec = inputSpec([
|
||||
z.string(),
|
||||
z.object({
|
||||
default: z.any().optional(),
|
||||
forceInput: z.boolean().optional(),
|
||||
}),
|
||||
]);
|
||||
forceInput: z.boolean().optional()
|
||||
})
|
||||
])
|
||||
|
||||
const zInputSpec = z.union([
|
||||
zIntInputSpec,
|
||||
@@ -246,14 +246,14 @@ const zInputSpec = z.union([
|
||||
zBooleanInputSpec,
|
||||
zStringInputSpec,
|
||||
zComboInputSpec,
|
||||
zCustomInputSpec,
|
||||
]);
|
||||
zCustomInputSpec
|
||||
])
|
||||
|
||||
const zComfyNodeDataType = z.string();
|
||||
const zComfyComboOutput = z.array(z.any());
|
||||
const zComfyNodeDataType = z.string()
|
||||
const zComfyComboOutput = z.array(z.any())
|
||||
const zComfyOutputSpec = z.array(
|
||||
z.union([zComfyNodeDataType, zComfyComboOutput])
|
||||
);
|
||||
)
|
||||
|
||||
const zComfyNodeDef = z.object({
|
||||
input: z.object({
|
||||
@@ -261,7 +261,7 @@ const zComfyNodeDef = z.object({
|
||||
optional: z.record(zInputSpec).optional(),
|
||||
// Frontend repo is not using it, but some custom nodes are using the
|
||||
// hidden field to pass various values.
|
||||
hidden: z.record(z.any()).optional(),
|
||||
hidden: z.record(z.any()).optional()
|
||||
}),
|
||||
output: zComfyOutputSpec,
|
||||
output_is_list: z.array(z.boolean()),
|
||||
@@ -271,23 +271,23 @@ const zComfyNodeDef = z.object({
|
||||
description: z.string(),
|
||||
category: z.string(),
|
||||
output_node: z.boolean(),
|
||||
python_module: z.string(),
|
||||
});
|
||||
python_module: z.string()
|
||||
})
|
||||
|
||||
// `/object_info`
|
||||
export type ComfyInputSpec = z.infer<typeof zInputSpec>;
|
||||
export type ComfyOutputSpec = z.infer<typeof zComfyOutputSpec>;
|
||||
export type ComfyNodeDef = z.infer<typeof zComfyNodeDef>;
|
||||
export type ComfyInputSpec = z.infer<typeof zInputSpec>
|
||||
export type ComfyOutputSpec = z.infer<typeof zComfyOutputSpec>
|
||||
export type ComfyNodeDef = z.infer<typeof zComfyNodeDef>
|
||||
|
||||
export function validateComfyNodeDef(data: any): ComfyNodeDef {
|
||||
const result = zComfyNodeDef.safeParse(data);
|
||||
const result = zComfyNodeDef.safeParse(data)
|
||||
if (!result.success) {
|
||||
const zodError = fromZodError(result.error);
|
||||
const zodError = fromZodError(result.error)
|
||||
const error = new Error(
|
||||
`Invalid ComfyNodeDef: ${JSON.stringify(data)}\n${zodError.message}`
|
||||
);
|
||||
error.cause = zodError;
|
||||
throw error;
|
||||
)
|
||||
error.cause = zodError
|
||||
throw error
|
||||
}
|
||||
return result.data;
|
||||
return result.data
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { z } from 'zod'
|
||||
|
||||
const nodeSlotSchema = z
|
||||
.object({
|
||||
@@ -26,9 +26,9 @@ const nodeSlotSchema = z
|
||||
TAESD: z.string(),
|
||||
TIMESTEP_KEYFRAME: z.string().optional(),
|
||||
UPSCALE_MODEL: z.string().optional(),
|
||||
VAE: z.string(),
|
||||
VAE: z.string()
|
||||
})
|
||||
.passthrough();
|
||||
.passthrough()
|
||||
|
||||
const litegraphBaseSchema = z
|
||||
.object({
|
||||
@@ -52,44 +52,44 @@ const litegraphBaseSchema = z
|
||||
WIDGET_SECONDARY_TEXT_COLOR: z.string(),
|
||||
LINK_COLOR: z.string(),
|
||||
EVENT_LINK_COLOR: z.string(),
|
||||
CONNECTING_LINK_COLOR: z.string(),
|
||||
CONNECTING_LINK_COLOR: z.string()
|
||||
})
|
||||
.passthrough();
|
||||
.passthrough()
|
||||
|
||||
const comfyBaseSchema = z.object({
|
||||
["fg-color"]: z.string(),
|
||||
["bg-color"]: z.string(),
|
||||
["comfy-menu-bg"]: z.string(),
|
||||
["comfy-input-bg"]: z.string(),
|
||||
["input-text"]: z.string(),
|
||||
["descrip-text"]: z.string(),
|
||||
["drag-text"]: z.string(),
|
||||
["error-text"]: z.string(),
|
||||
["border-color"]: z.string(),
|
||||
["tr-even-bg-color"]: z.string(),
|
||||
["tr-odd-bg-color"]: z.string(),
|
||||
["content-bg"]: z.string(),
|
||||
["content-fg"]: z.string(),
|
||||
["content-hover-bg"]: z.string(),
|
||||
["content-hover-fg"]: z.string(),
|
||||
});
|
||||
['fg-color']: z.string(),
|
||||
['bg-color']: z.string(),
|
||||
['comfy-menu-bg']: z.string(),
|
||||
['comfy-input-bg']: z.string(),
|
||||
['input-text']: z.string(),
|
||||
['descrip-text']: z.string(),
|
||||
['drag-text']: z.string(),
|
||||
['error-text']: z.string(),
|
||||
['border-color']: z.string(),
|
||||
['tr-even-bg-color']: z.string(),
|
||||
['tr-odd-bg-color']: z.string(),
|
||||
['content-bg']: z.string(),
|
||||
['content-fg']: z.string(),
|
||||
['content-hover-bg']: z.string(),
|
||||
['content-hover-fg']: z.string()
|
||||
})
|
||||
|
||||
const colorsSchema = z
|
||||
.object({
|
||||
node_slot: nodeSlotSchema,
|
||||
litegraph_base: litegraphBaseSchema,
|
||||
comfy_base: comfyBaseSchema,
|
||||
comfy_base: comfyBaseSchema
|
||||
})
|
||||
.passthrough();
|
||||
.passthrough()
|
||||
|
||||
const paletteSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
colors: colorsSchema,
|
||||
});
|
||||
colors: colorsSchema
|
||||
})
|
||||
|
||||
const colorPalettesSchema = z.record(paletteSchema);
|
||||
const colorPalettesSchema = z.record(paletteSchema)
|
||||
|
||||
export type Colors = z.infer<typeof colorsSchema>;
|
||||
export type Palette = z.infer<typeof paletteSchema>;
|
||||
export type ColorPalettes = z.infer<typeof colorPalettesSchema>;
|
||||
export type Colors = z.infer<typeof colorsSchema>
|
||||
export type Palette = z.infer<typeof paletteSchema>
|
||||
export type ColorPalettes = z.infer<typeof colorPalettesSchema>
|
||||
|
||||
44
src/types/comfy.d.ts
vendored
44
src/types/comfy.d.ts
vendored
@@ -1,21 +1,21 @@
|
||||
import { LGraphNode, IWidget } from "./litegraph";
|
||||
import { ComfyApp } from "../../scripts/app";
|
||||
import { LGraphNode, IWidget } from './litegraph'
|
||||
import { ComfyApp } from '../../scripts/app'
|
||||
|
||||
export interface ComfyExtension {
|
||||
/**
|
||||
* The name of the extension
|
||||
*/
|
||||
name: string;
|
||||
name: string
|
||||
/**
|
||||
* Allows any initialisation, e.g. loading resources. Called after the canvas is created but before nodes are added
|
||||
* @param app The ComfyUI app instance
|
||||
*/
|
||||
init?(app: ComfyApp): Promise<void>;
|
||||
init?(app: ComfyApp): Promise<void>
|
||||
/**
|
||||
* Allows any additonal setup, called after the application is fully set up and running
|
||||
* @param app The ComfyUI app instance
|
||||
*/
|
||||
setup?(app: ComfyApp): Promise<void>;
|
||||
setup?(app: ComfyApp): Promise<void>
|
||||
/**
|
||||
* Called before nodes are registered with the graph
|
||||
* @param defs The collection of node definitions, add custom ones or edit existing ones
|
||||
@@ -24,7 +24,7 @@ export interface ComfyExtension {
|
||||
addCustomNodeDefs?(
|
||||
defs: Record<string, ComfyObjectInfo>,
|
||||
app: ComfyApp
|
||||
): Promise<void>;
|
||||
): Promise<void>
|
||||
/**
|
||||
* Allows the extension to add custom widgets
|
||||
* @param app The ComfyUI app instance
|
||||
@@ -42,7 +42,7 @@ export interface ComfyExtension {
|
||||
app
|
||||
) => { widget?: IWidget; minWidth?: number; minHeight?: number }
|
||||
>
|
||||
>;
|
||||
>
|
||||
/**
|
||||
* Allows the extension to add additional handling to the node before it is registered with LGraph
|
||||
* @param nodeType The node class (not an instance)
|
||||
@@ -53,12 +53,12 @@ export interface ComfyExtension {
|
||||
nodeType: typeof LGraphNode,
|
||||
nodeData: ComfyObjectInfo,
|
||||
app: ComfyApp
|
||||
): Promise<void>;
|
||||
): Promise<void>
|
||||
/**
|
||||
* Allows the extension to register additional nodes with LGraph after standard nodes are added
|
||||
* @param app The ComfyUI app instance
|
||||
*/
|
||||
registerCustomNodes?(app: ComfyApp): Promise<void>;
|
||||
registerCustomNodes?(app: ComfyApp): Promise<void>
|
||||
/**
|
||||
* Allows the extension to modify a node that has been reloaded onto the graph.
|
||||
* If you break something in the backend and want to patch workflows in the frontend
|
||||
@@ -66,26 +66,26 @@ export interface ComfyExtension {
|
||||
* @param node The node that has been loaded
|
||||
* @param app The ComfyUI app instance
|
||||
*/
|
||||
loadedGraphNode?(node: LGraphNode, app: ComfyApp);
|
||||
loadedGraphNode?(node: LGraphNode, app: ComfyApp)
|
||||
/**
|
||||
* Allows the extension to run code after the constructor of the node
|
||||
* @param node The node that has been created
|
||||
* @param app The ComfyUI app instance
|
||||
*/
|
||||
nodeCreated?(node: LGraphNode, app: ComfyApp);
|
||||
nodeCreated?(node: LGraphNode, app: ComfyApp)
|
||||
}
|
||||
|
||||
export type ComfyObjectInfo = {
|
||||
name: string;
|
||||
display_name?: string;
|
||||
description?: string;
|
||||
category: string;
|
||||
name: string
|
||||
display_name?: string
|
||||
description?: string
|
||||
category: string
|
||||
input?: {
|
||||
required?: Record<string, ComfyObjectInfoConfig>;
|
||||
optional?: Record<string, ComfyObjectInfoConfig>;
|
||||
};
|
||||
output?: string[];
|
||||
output_name: string[];
|
||||
};
|
||||
required?: Record<string, ComfyObjectInfoConfig>
|
||||
optional?: Record<string, ComfyObjectInfoConfig>
|
||||
}
|
||||
output?: string[]
|
||||
output_name: string[]
|
||||
}
|
||||
|
||||
export type ComfyObjectInfoConfig = [string | any[]] | [string | any[], any];
|
||||
export type ComfyObjectInfoConfig = [string | any[]] | [string | any[], any]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { z } from 'zod'
|
||||
import { fromZodError } from 'zod-validation-error'
|
||||
|
||||
const zComfyLink = z.tuple([
|
||||
z.number(), // Link id
|
||||
@@ -7,26 +7,26 @@ const zComfyLink = z.tuple([
|
||||
z.number(), // Output slot# of source node
|
||||
z.number(), // Node id of destination node
|
||||
z.number(), // Input slot# of destination node
|
||||
z.string(), // Data type
|
||||
]);
|
||||
z.string() // Data type
|
||||
])
|
||||
|
||||
const zNodeOutput = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
links: z.array(z.number()).nullable(),
|
||||
slot_index: z.number().optional(),
|
||||
slot_index: z.number().optional()
|
||||
})
|
||||
.passthrough();
|
||||
.passthrough()
|
||||
|
||||
const zNodeInput = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
link: z.number().nullable(),
|
||||
slot_index: z.number().optional(),
|
||||
slot_index: z.number().optional()
|
||||
})
|
||||
.passthrough();
|
||||
.passthrough()
|
||||
|
||||
const zFlags = z
|
||||
.object({
|
||||
@@ -34,22 +34,22 @@ const zFlags = z
|
||||
pinned: z.boolean().optional(),
|
||||
allow_interaction: z.boolean().optional(),
|
||||
horizontal: z.boolean().optional(),
|
||||
skip_repeated_outputs: z.boolean().optional(),
|
||||
skip_repeated_outputs: z.boolean().optional()
|
||||
})
|
||||
.passthrough();
|
||||
.passthrough()
|
||||
|
||||
const zProperties = z
|
||||
.object({
|
||||
["Node name for S&R"]: z.string().optional(),
|
||||
['Node name for S&R']: z.string().optional()
|
||||
})
|
||||
.passthrough();
|
||||
.passthrough()
|
||||
|
||||
const zVector2 = z.union([
|
||||
z.object({ 0: z.number(), 1: z.number() }).transform((v) => [v[0], v[1]]),
|
||||
z.tuple([z.number(), z.number()]),
|
||||
]);
|
||||
z.tuple([z.number(), z.number()])
|
||||
])
|
||||
|
||||
const zWidgetValues = z.union([z.array(z.any()), z.record(z.any())]);
|
||||
const zWidgetValues = z.union([z.array(z.any()), z.record(z.any())])
|
||||
|
||||
const zComfyNode = z
|
||||
.object({
|
||||
@@ -65,9 +65,9 @@ const zComfyNode = z
|
||||
properties: zProperties,
|
||||
widgets_values: zWidgetValues.optional(),
|
||||
color: z.string().optional(),
|
||||
bgcolor: z.string().optional(),
|
||||
bgcolor: z.string().optional()
|
||||
})
|
||||
.passthrough();
|
||||
.passthrough()
|
||||
|
||||
const zGroup = z
|
||||
.object({
|
||||
@@ -75,9 +75,9 @@ const zGroup = z
|
||||
bounding: z.tuple([z.number(), z.number(), z.number(), z.number()]),
|
||||
color: z.string(),
|
||||
font_size: z.number(),
|
||||
locked: z.boolean().optional(),
|
||||
locked: z.boolean().optional()
|
||||
})
|
||||
.passthrough();
|
||||
.passthrough()
|
||||
|
||||
const zInfo = z
|
||||
.object({
|
||||
@@ -87,30 +87,30 @@ const zInfo = z
|
||||
version: z.string(),
|
||||
created: z.string(),
|
||||
modified: z.string(),
|
||||
software: z.string(),
|
||||
software: z.string()
|
||||
})
|
||||
.passthrough();
|
||||
.passthrough()
|
||||
|
||||
const zDS = z
|
||||
.object({
|
||||
scale: z.number(),
|
||||
offset: zVector2,
|
||||
offset: zVector2
|
||||
})
|
||||
.passthrough();
|
||||
.passthrough()
|
||||
|
||||
const zConfig = z
|
||||
.object({
|
||||
links_ontop: z.boolean().optional(),
|
||||
align_to_grid: z.boolean().optional(),
|
||||
align_to_grid: z.boolean().optional()
|
||||
})
|
||||
.passthrough();
|
||||
.passthrough()
|
||||
|
||||
const zExtra = z
|
||||
.object({
|
||||
ds: zDS.optional(),
|
||||
info: zInfo.optional(),
|
||||
info: zInfo.optional()
|
||||
})
|
||||
.passthrough();
|
||||
.passthrough()
|
||||
|
||||
export const zComfyWorkflow = z
|
||||
.object({
|
||||
@@ -121,26 +121,26 @@ export const zComfyWorkflow = z
|
||||
groups: z.array(zGroup).optional(),
|
||||
config: zConfig.optional().nullable(),
|
||||
extra: zExtra.optional().nullable(),
|
||||
version: z.number(),
|
||||
version: z.number()
|
||||
})
|
||||
.passthrough();
|
||||
.passthrough()
|
||||
|
||||
export type NodeInput = z.infer<typeof zNodeInput>;
|
||||
export type NodeOutput = z.infer<typeof zNodeOutput>;
|
||||
export type ComfyLink = z.infer<typeof zComfyLink>;
|
||||
export type ComfyNode = z.infer<typeof zComfyNode>;
|
||||
export type ComfyWorkflowJSON = z.infer<typeof zComfyWorkflow>;
|
||||
export type NodeInput = z.infer<typeof zNodeInput>
|
||||
export type NodeOutput = z.infer<typeof zNodeOutput>
|
||||
export type ComfyLink = z.infer<typeof zComfyLink>
|
||||
export type ComfyNode = z.infer<typeof zComfyNode>
|
||||
export type ComfyWorkflowJSON = z.infer<typeof zComfyWorkflow>
|
||||
|
||||
export async function parseComfyWorkflow(
|
||||
data: string
|
||||
): Promise<ComfyWorkflowJSON> {
|
||||
// Validate
|
||||
const result = await zComfyWorkflow.safeParseAsync(JSON.parse(data));
|
||||
const result = await zComfyWorkflow.safeParseAsync(JSON.parse(data))
|
||||
if (!result.success) {
|
||||
// TODO: Pretty print the error on UI modal.
|
||||
const error = fromZodError(result.error);
|
||||
alert(`Invalid workflow against zod schema:\n${error}`);
|
||||
throw error;
|
||||
const error = fromZodError(result.error)
|
||||
alert(`Invalid workflow against zod schema:\n${error}`)
|
||||
throw error
|
||||
}
|
||||
return result.data;
|
||||
return result.data
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import { Component } from "vue";
|
||||
import { Component } from 'vue'
|
||||
|
||||
export interface BaseSidebarTabExtension {
|
||||
id: string;
|
||||
title: string;
|
||||
icon?: string;
|
||||
order?: number;
|
||||
tooltip?: string;
|
||||
id: string
|
||||
title: string
|
||||
icon?: string
|
||||
order?: number
|
||||
tooltip?: string
|
||||
}
|
||||
|
||||
export interface VueSidebarTabExtension extends BaseSidebarTabExtension {
|
||||
type: "vue";
|
||||
component: Component;
|
||||
type: 'vue'
|
||||
component: Component
|
||||
}
|
||||
|
||||
export interface CustomSidebarTabExtension extends BaseSidebarTabExtension {
|
||||
type: "custom";
|
||||
render: (container: HTMLElement) => void;
|
||||
destroy?: () => void;
|
||||
type: 'custom'
|
||||
render: (container: HTMLElement) => void
|
||||
destroy?: () => void
|
||||
}
|
||||
|
||||
export type SidebarTabExtension =
|
||||
| VueSidebarTabExtension
|
||||
| CustomSidebarTabExtension;
|
||||
| CustomSidebarTabExtension
|
||||
|
||||
export interface ExtensionManager {
|
||||
registerSidebarTab(tab: SidebarTabExtension): void;
|
||||
unregisterSidebarTab(id: string): void;
|
||||
getSidebarTabs(): SidebarTabExtension[];
|
||||
registerSidebarTab(tab: SidebarTabExtension): void
|
||||
unregisterSidebarTab(id: string): void
|
||||
getSidebarTabs(): SidebarTabExtension[]
|
||||
}
|
||||
|
||||
20
src/types/litegraph-augmentation.d.ts
vendored
20
src/types/litegraph-augmentation.d.ts
vendored
@@ -1,46 +1,46 @@
|
||||
import "@comfyorg/litegraph";
|
||||
import '@comfyorg/litegraph'
|
||||
|
||||
/**
|
||||
* ComfyUI extensions of litegraph
|
||||
*/
|
||||
declare module "@comfyorg/litegraph" {
|
||||
declare module '@comfyorg/litegraph' {
|
||||
interface LGraphNode {
|
||||
/**
|
||||
* Callback fired on each node after the graph is configured
|
||||
*/
|
||||
onAfterGraphConfigured?(): void;
|
||||
onAfterGraphConfigured?(): void
|
||||
|
||||
/**
|
||||
* If the node is a frontend only node and should not be serialized into the prompt.
|
||||
*/
|
||||
isVirtualNode?: boolean;
|
||||
isVirtualNode?: boolean
|
||||
}
|
||||
|
||||
interface IWidget<TValue = any, TOptions = any> {
|
||||
/**
|
||||
* Allows for additional cleanup when removing a widget when converting to input.
|
||||
*/
|
||||
onRemove?(): void;
|
||||
onRemove?(): void
|
||||
}
|
||||
|
||||
interface INodeOutputSlot {
|
||||
widget?: unknown;
|
||||
widget?: unknown
|
||||
}
|
||||
|
||||
interface INodeInputSlot {
|
||||
widget?: unknown;
|
||||
widget?: unknown
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended types for litegraph, to be merged upstream once it has stabilized.
|
||||
*/
|
||||
declare module "@comfyorg/litegraph" {
|
||||
declare module '@comfyorg/litegraph' {
|
||||
interface INodeInputSlot {
|
||||
pos?: [number, number];
|
||||
pos?: [number, number]
|
||||
}
|
||||
|
||||
interface LGraphNode {
|
||||
widgets_values?: unknown[];
|
||||
widgets_values?: unknown[]
|
||||
}
|
||||
}
|
||||
|
||||
22
src/types/litegraph-core-augmentation.d.ts
vendored
22
src/types/litegraph-core-augmentation.d.ts
vendored
@@ -2,18 +2,18 @@
|
||||
Extended types for litegraph, to be merged upstream once it has stabilized.
|
||||
Augmenting the LiteGraph type really didn't want to work, however doing it like this seems to allow it.
|
||||
*/
|
||||
declare module "@comfyorg/litegraph" {
|
||||
declare module '@comfyorg/litegraph' {
|
||||
interface LiteGraphExtended {
|
||||
search_filter_enabled: boolean;
|
||||
middle_click_slot_add_default_node: boolean;
|
||||
registered_slot_out_types: Record<string, { nodes: string[] }>;
|
||||
registered_slot_in_types: Record<string, { nodes: string[] }>;
|
||||
slot_types_out: string[];
|
||||
slot_types_default_out: Record<string, string[]>;
|
||||
slot_types_default_in: Record<string, string[]>;
|
||||
search_filter_enabled: boolean
|
||||
middle_click_slot_add_default_node: boolean
|
||||
registered_slot_out_types: Record<string, { nodes: string[] }>
|
||||
registered_slot_in_types: Record<string, { nodes: string[] }>
|
||||
slot_types_out: string[]
|
||||
slot_types_default_out: Record<string, string[]>
|
||||
slot_types_default_in: Record<string, string[]>
|
||||
}
|
||||
|
||||
import type { LiteGraph as LG } from "@comfyorg/litegraph/src/litegraph";
|
||||
export const LiteGraph: LiteGraphExtended & typeof LG;
|
||||
export * from "@comfyorg/litegraph/src/litegraph";
|
||||
import type { LiteGraph as LG } from '@comfyorg/litegraph/src/litegraph'
|
||||
export const LiteGraph: LiteGraphExtended & typeof LG
|
||||
export * from '@comfyorg/litegraph/src/litegraph'
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
export type NodeSourceType = "core" | "custom_nodes";
|
||||
export type NodeSourceType = 'core' | 'custom_nodes'
|
||||
export type NodeSource = {
|
||||
type: NodeSourceType;
|
||||
className: string;
|
||||
displayText: string;
|
||||
};
|
||||
type: NodeSourceType
|
||||
className: string
|
||||
displayText: string
|
||||
}
|
||||
|
||||
export const getNodeSource = (python_module: string): NodeSource => {
|
||||
const modules = python_module.split(".");
|
||||
if (["nodes", "comfy_extras"].includes(modules[0])) {
|
||||
const modules = python_module.split('.')
|
||||
if (['nodes', 'comfy_extras'].includes(modules[0])) {
|
||||
return {
|
||||
type: "core",
|
||||
className: "comfy-core",
|
||||
displayText: "Comfy Core",
|
||||
};
|
||||
} else if (modules[0] === "custom_nodes") {
|
||||
return {
|
||||
type: "custom_nodes",
|
||||
className: "comfy-custom-nodes",
|
||||
displayText: modules[1],
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Unknown node source: ${python_module}`);
|
||||
type: 'core',
|
||||
className: 'comfy-core',
|
||||
displayText: 'Comfy Core'
|
||||
}
|
||||
};
|
||||
} else if (modules[0] === 'custom_nodes') {
|
||||
return {
|
||||
type: 'custom_nodes',
|
||||
className: 'comfy-custom-nodes',
|
||||
displayText: modules[1]
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unknown node source: ${python_module}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
export type StorageLocation = "browser" | "server";
|
||||
export type StorageLocation = 'browser' | 'server'
|
||||
|
||||
export type SettingInputType =
|
||||
| "boolean"
|
||||
| "number"
|
||||
| "slider"
|
||||
| "combo"
|
||||
| "text"
|
||||
| "hidden";
|
||||
| 'boolean'
|
||||
| 'number'
|
||||
| 'slider'
|
||||
| 'combo'
|
||||
| 'text'
|
||||
| 'hidden'
|
||||
|
||||
export type SettingCustomRenderer = (
|
||||
name: string,
|
||||
setter: (v: any) => void,
|
||||
value: any,
|
||||
attrs: any
|
||||
) => HTMLElement;
|
||||
) => HTMLElement
|
||||
|
||||
export interface SettingOption {
|
||||
text: string;
|
||||
value?: string;
|
||||
text: string
|
||||
value?: string
|
||||
}
|
||||
|
||||
export interface Setting {
|
||||
id: string;
|
||||
onChange?: (value: any, oldValue?: any) => void;
|
||||
name: string;
|
||||
render: () => HTMLElement;
|
||||
id: string
|
||||
onChange?: (value: any, oldValue?: any) => void
|
||||
name: string
|
||||
render: () => HTMLElement
|
||||
}
|
||||
|
||||
export interface SettingParams {
|
||||
id: string;
|
||||
name: string;
|
||||
type: SettingInputType | SettingCustomRenderer;
|
||||
defaultValue: any;
|
||||
onChange?: (newValue: any, oldValue?: any) => void;
|
||||
attrs?: any;
|
||||
tooltip?: string;
|
||||
options?: Array<string | SettingOption> | ((value: any) => SettingOption[]);
|
||||
id: string
|
||||
name: string
|
||||
type: SettingInputType | SettingCustomRenderer
|
||||
defaultValue: any
|
||||
onChange?: (newValue: any, oldValue?: any) => void
|
||||
attrs?: any
|
||||
tooltip?: string
|
||||
options?: Array<string | SettingOption> | ((value: any) => SettingOption[])
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user