Compare commits

..

2 Commits

Author SHA1 Message Date
filtered
782d93a7a0 Add awaits to various tests 2024-11-15 01:28:48 +11:00
filtered
7be14c5189 Add nodeTemplate tests
Test failure confirmed when links are not connected
2024-11-15 01:28:18 +11:00
98 changed files with 1687 additions and 8218 deletions

View File

@@ -6,11 +6,6 @@ PLAYWRIGHT_TEST_URL=http://localhost:5173
# Note: localhost:8188 does not work. # Note: localhost:8188 does not work.
DEV_SERVER_COMFYUI_URL=http://127.0.0.1:8188 DEV_SERVER_COMFYUI_URL=http://127.0.0.1:8188
# Allow dev server access from remote IP addresses.
# If true, the vite dev server will listen on all addresses, including LAN
# and public addresses.
VITE_REMOTE_DEV=false
# The target ComfyUI checkout directory to deploy the frontend code to. # The target ComfyUI checkout directory to deploy the frontend code to.
# The dist directory will be copied to {DEPLOY_COMFYUI_DIR}/custom_web_versions/main/dev # The dist directory will be copied to {DEPLOY_COMFYUI_DIR}/custom_web_versions/main/dev
# Add `--front-end-root {DEPLOY_COMFYUI_DIR}/custom_web_versions/main/dev` # Add `--front-end-root {DEPLOY_COMFYUI_DIR}/custom_web_versions/main/dev`

View File

@@ -20,7 +20,7 @@ jobs:
run: | run: |
npm run test:generate npm run test:generate
npm run test:generate:examples npm run test:generate:examples
npm run test:jest:fast -- --verbose npm test -- --verbose
working-directory: ComfyUI_frontend working-directory: ComfyUI_frontend
playwright-tests-chromium: playwright-tests-chromium:

View File

@@ -431,8 +431,6 @@ core extensions will be loaded.
#### Access dev server on touch devices #### Access dev server on touch devices
Enable remote access to the dev server by setting `VITE_REMOTE_DEV` in `.env` to `true`.
After you start the dev server, you should see following logs: After you start the dev server, you should see following logs:
``` ```

View File

@@ -0,0 +1,10 @@
[
{
"name": "Three Nodes Template",
"data": "{\"nodes\":[{\"id\":7,\"type\":\"CLIPTextEncode\",\"pos\":[413,389],\"size\":[425.27801513671875,180.6060791015625],\"flags\":{},\"order\":3,\"mode\":0,\"inputs\":[{\"name\":\"clip\",\"type\":\"CLIP\",\"link\":null}],\"outputs\":[{\"name\":\"CONDITIONING\",\"type\":\"CONDITIONING\",\"links\":[],\"slot_index\":0}],\"properties\":{\"Node name for S&R\":\"CLIPTextEncode\"},\"widgets_values\":[\"text, watermark\"]},{\"id\":6,\"type\":\"CLIPTextEncode\",\"pos\":[415,186],\"size\":[422.84503173828125,164.31304931640625],\"flags\":{},\"order\":2,\"mode\":0,\"inputs\":[{\"name\":\"clip\",\"type\":\"CLIP\",\"link\":null}],\"outputs\":[{\"name\":\"CONDITIONING\",\"type\":\"CONDITIONING\",\"links\":[],\"slot_index\":0}],\"properties\":{\"Node name for S&R\":\"CLIPTextEncode\"},\"widgets_values\":[\"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,\"]},{\"id\":4,\"type\":\"CheckpointLoaderSimple\",\"pos\":[26,474],\"size\":[315,98],\"flags\":{},\"order\":1,\"mode\":0,\"inputs\":[],\"outputs\":[{\"name\":\"MODEL\",\"type\":\"MODEL\",\"links\":[],\"slot_index\":0},{\"name\":\"CLIP\",\"type\":\"CLIP\",\"links\":[],\"slot_index\":1},{\"name\":\"VAE\",\"type\":\"VAE\",\"links\":[],\"slot_index\":2}],\"properties\":{\"Node name for S&R\":\"CheckpointLoaderSimple\"},\"widgets_values\":[\"v1-5-pruned-emaonly.ckpt\"]}],\"groups\":[],\"reroutes\":[],\"links\":[{\"id\":5,\"origin_id\":4,\"origin_slot\":1,\"target_id\":7,\"target_slot\":0,\"type\":\"CLIP\"},{\"id\":3,\"origin_id\":4,\"origin_slot\":1,\"target_id\":6,\"target_slot\":0,\"type\":\"CLIP\"}]}"
},
{
"name": "Completely empty template",
"data": "{\"nodes\":[],\"groups\":[],\"reroutes\":[],\"links\":[]}"
}
]

View File

@@ -0,0 +1,6 @@
[
{
"name": "vintageClipboard Template",
"data": "{\"nodes\":[{\"id\":-1,\"type\":\"CheckpointLoaderSimple\",\"pos\":[26,474],\"size\":[315,98],\"flags\":{},\"order\":1,\"mode\":0,\"inputs\":[],\"outputs\":[{\"name\":\"MODEL\",\"type\":\"MODEL\",\"links\":[],\"slot_index\":0},{\"name\":\"CLIP\",\"type\":\"CLIP\",\"links\":[],\"slot_index\":1},{\"name\":\"VAE\",\"type\":\"VAE\",\"links\":[],\"slot_index\":2}],\"properties\":{\"Node name for S&R\":\"CheckpointLoaderSimple\"},\"widgets_values\":[\"v1-5-pruned-emaonly.ckpt\"]},{\"id\":-1,\"type\":\"CLIPTextEncode\",\"pos\":[415,186],\"size\":[422.84503173828125,164.31304931640625],\"flags\":{},\"order\":2,\"mode\":0,\"inputs\":[{\"name\":\"clip\",\"type\":\"CLIP\",\"link\":null}],\"outputs\":[{\"name\":\"CONDITIONING\",\"type\":\"CONDITIONING\",\"links\":[],\"slot_index\":0}],\"properties\":{\"Node name for S&R\":\"CLIPTextEncode\"},\"widgets_values\":[\"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,\"]},{\"id\":-1,\"type\":\"CLIPTextEncode\",\"pos\":[413,389],\"size\":[425.27801513671875,180.6060791015625],\"flags\":{},\"order\":3,\"mode\":0,\"inputs\":[{\"name\":\"clip\",\"type\":\"CLIP\",\"link\":null}],\"outputs\":[{\"name\":\"CONDITIONING\",\"type\":\"CONDITIONING\",\"links\":[],\"slot_index\":0}],\"properties\":{\"Node name for S&R\":\"CLIPTextEncode\"},\"widgets_values\":[\"text, watermark\"]}],\"links\":[[0,1,1,0,4],[0,1,2,0,4]]}"
}
]

View File

@@ -62,7 +62,6 @@ test.describe('Change Tracker', () => {
expect(await getRedoQueueSize()).toBe(0) expect(await getRedoQueueSize()).toBe(0)
const node = (await comfyPage.getFirstNodeRef())! const node = (await comfyPage.getFirstNodeRef())!
await node.click('title')
await node.click('collapse') await node.click('collapse')
await expect(node).toBeCollapsed() await expect(node).toBeCollapsed()
expect(await isModified()).toBe(true) expect(await isModified()).toBe(true)
@@ -99,7 +98,6 @@ test.describe('Change Tracker', () => {
// Make changes outside set // Make changes outside set
// Bypass + collapse node // Bypass + collapse node
await node.click('title')
await node.click('collapse') await node.click('collapse')
await comfyPage.ctrlB() await comfyPage.ctrlB()
await expect(node).toBeCollapsed() await expect(node).toBeCollapsed()
@@ -113,10 +111,6 @@ test.describe('Change Tracker', () => {
await expect(node).not.toBeBypassed() await expect(node).not.toBeBypassed()
await expect(node).not.toBeCollapsed() await expect(node).not.toBeCollapsed()
// Prevent clicks registering a double-click
await comfyPage.clickEmptySpace()
await node.click('title')
// Run again, but within a change transaction // Run again, but within a change transaction
await beforeChange(comfyPage) await beforeChange(comfyPage)
@@ -158,7 +152,6 @@ test.describe('Change Tracker', () => {
const multipleChanges = async () => { const multipleChanges = async () => {
await beforeChange(comfyPage) await beforeChange(comfyPage)
// Call other actions that uses begin/endChange // Call other actions that uses begin/endChange
await node.click('title')
await collapse() await collapse()
await bypassAndPin() await bypassAndPin()
await afterChange(comfyPage) await afterChange(comfyPage)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 152 KiB

View File

@@ -24,14 +24,6 @@ test.describe('Copy Paste', () => {
test('Can copy and paste widget value', async ({ comfyPage }) => { test('Can copy and paste widget value', async ({ comfyPage }) => {
// Copy width value (512) from empty latent node to KSampler's seed. // Copy width value (512) from empty latent node to KSampler's seed.
// KSampler's seed
await comfyPage.canvas.click({
position: {
x: 1005,
y: 281
}
})
await comfyPage.ctrlC(null)
// Empty latent node's width // Empty latent node's width
await comfyPage.canvas.click({ await comfyPage.canvas.click({
position: { position: {
@@ -39,6 +31,14 @@ test.describe('Copy Paste', () => {
y: 643 y: 643
} }
}) })
await comfyPage.ctrlC(null)
// KSampler's seed
await comfyPage.canvas.click({
position: {
x: 1005,
y: 281
}
})
await comfyPage.ctrlV(null) await comfyPage.ctrlV(null)
await comfyPage.page.keyboard.press('Enter') await comfyPage.page.keyboard.press('Enter')
await expect(comfyPage.canvas).toHaveScreenshot('copied-widget-value.png') await expect(comfyPage.canvas).toHaveScreenshot('copied-widget-value.png')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -94,7 +94,7 @@ test.describe('Settings', () => {
test('Can change canvas zoom speed setting', async ({ comfyPage }) => { test('Can change canvas zoom speed setting', async ({ comfyPage }) => {
const maxSpeed = 2.5 const maxSpeed = 2.5
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed) await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed)
test.step('Setting should persist', async () => { await test.step('Setting should persist', async () => {
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed) expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed)
}) })
}) })

View File

@@ -77,6 +77,7 @@ export class ComfyPage {
// All canvas position operations are based on default view of canvas. // All canvas position operations are based on default view of canvas.
public readonly canvas: Locator public readonly canvas: Locator
public readonly widgetTextBox: Locator public readonly widgetTextBox: Locator
public readonly contextMenu: Locator
// Buttons // Buttons
public readonly resetViewButton: Locator public readonly resetViewButton: Locator
@@ -107,6 +108,7 @@ export class ComfyPage {
this.url = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188' this.url = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'
this.canvas = page.locator('#graph-canvas') this.canvas = page.locator('#graph-canvas')
this.widgetTextBox = page.getByPlaceholder('text').nth(1) this.widgetTextBox = page.getByPlaceholder('text').nth(1)
this.contextMenu = page.locator('.litegraph.litecontextmenu')
this.resetViewButton = page.getByRole('button', { name: 'Reset View' }) this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
this.queueButton = page.getByRole('button', { name: 'Queue Prompt' }) this.queueButton = page.getByRole('button', { name: 'Queue Prompt' })
this.workflowUploadInput = page.locator('#comfy-file-input') this.workflowUploadInput = page.locator('#comfy-file-input')
@@ -148,6 +150,12 @@ export class ComfyPage {
}) })
} }
async getGraphSelectedItemsCount(): Promise<number | undefined> {
return await this.page.evaluate(() => {
return window['app']?.canvas?.selectedItems?.size
})
}
async setupWorkflowsDirectory(structure: FolderStructure) { async setupWorkflowsDirectory(structure: FolderStructure) {
const resp = await this.request.post( const resp = await this.request.post(
`${this.url}/api/devtools/setup_folder_structure`, `${this.url}/api/devtools/setup_folder_structure`,
@@ -191,6 +199,39 @@ export class ComfyPage {
return await resp.json() return await resp.json()
} }
async clearNodeTemplates() {
const resp = await this.request.delete(
`${this.url}/api/userdata/comfy.templates.json`,
{
headers: { 'Comfy-User': this.id }
}
)
const status = resp.status()
if (status !== 204 && status !== 404)
throw new Error(`Failed to delete node templates: ${await resp.text()}`)
}
async setNodeTemplates(fileName: string) {
const path = this.assetPath(fileName)
const data = fs.readFileSync(path, 'utf-8')
const resp = await this.request.post(
`${this.url}/api/userdata/comfy.templates.json`,
{
headers: {
'Comfy-User': this.id,
overwrite: 'true',
full_info: 'true'
},
data
}
)
if (resp.status() !== 200)
throw new Error(`Failed to upload node templates: ${await resp.text()}`)
}
async setupSettings(settings: Record<string, any>) { async setupSettings(settings: Record<string, any>) {
const resp = await this.request.post( const resp = await this.request.post(
`${this.url}/api/devtools/set_settings`, `${this.url}/api/devtools/set_settings`,
@@ -204,15 +245,13 @@ export class ComfyPage {
} }
} }
async setup({ clearStorage = true }: { clearStorage?: boolean } = {}) { async setup() {
await this.goto() await this.goto()
if (clearStorage) { await this.page.evaluate((id) => {
await this.page.evaluate((id) => { localStorage.clear()
localStorage.clear() sessionStorage.clear()
sessionStorage.clear() localStorage.setItem('Comfy.userId', id)
localStorage.setItem('Comfy.userId', id) }, this.id)
}, this.id)
}
await this.goto() await this.goto()
// Unify font for consistent screenshots. // Unify font for consistent screenshots.
@@ -316,9 +355,9 @@ export class ComfyPage {
}, settingId) }, settingId)
} }
async reload({ clearStorage = true }: { clearStorage?: boolean } = {}) { async reload() {
await this.page.reload({ timeout: 15000 }) await this.page.reload({ timeout: 15000 })
await this.setup({ clearStorage }) await this.setup()
} }
async goto() { async goto() {
@@ -401,11 +440,17 @@ export class ComfyPage {
await this.nextFrame() await this.nextFrame()
} }
async dragAndDrop(source: Position, target: Position) { async dragAndDrop(
source: Position,
target: Position,
modifierKey?: 'ControlOrMeta' | 'Control' | 'Alt' | 'Shift'
) {
if (modifierKey) await this.page.keyboard.down(modifierKey)
await this.page.mouse.move(source.x, source.y) await this.page.mouse.move(source.x, source.y)
await this.page.mouse.down() await this.page.mouse.down()
await this.page.mouse.move(target.x, target.y) await this.page.mouse.move(target.x, target.y)
await this.page.mouse.up() await this.page.mouse.up()
if (modifierKey) await this.page.keyboard.up(modifierKey)
await this.nextFrame() await this.nextFrame()
} }
@@ -526,6 +571,9 @@ export class ComfyPage {
safeSpot = safeSpot || { x: 10, y: 10 } safeSpot = safeSpot || { x: 10, y: 10 }
await this.page.mouse.move(safeSpot.x, safeSpot.y) await this.page.mouse.move(safeSpot.x, safeSpot.y)
await this.page.mouse.down() await this.page.mouse.down()
// TEMPORARY HACK: Multiple pans open the search menu, so cheat and keep it closed.
// TODO: Fix that (double-click at not-the-same-coordinations should not open the menu)
await this.page.keyboard.press('Escape')
await this.page.mouse.move(offset.x + safeSpot.x, offset.y + safeSpot.y) await this.page.mouse.move(offset.x + safeSpot.x, offset.y + safeSpot.y)
await this.page.mouse.up() await this.page.mouse.up()
await this.nextFrame() await this.nextFrame()
@@ -550,13 +598,11 @@ export class ComfyPage {
} }
async rightClickCanvas() { async rightClickCanvas() {
await this.page.mouse.click(10, 10, { button: 'right' }) await this.canvas.click({
await this.nextFrame() position: { x: 10, y: 10 },
} button: 'right'
})
async clickContextMenuItem(name: string): Promise<void> { await expect(this.contextMenu).toBeVisible()
await this.page.getByRole('menuitem', { name }).click()
await this.nextFrame()
} }
async doubleClickCanvas() { async doubleClickCanvas() {
@@ -571,7 +617,7 @@ export class ComfyPage {
y: 625 y: 625
} }
}) })
this.page.mouse.move(10, 10) await this.page.mouse.move(10, 10)
await this.nextFrame() await this.nextFrame()
} }
@@ -583,10 +629,14 @@ export class ComfyPage {
}, },
button: 'right' button: 'right'
}) })
this.page.mouse.move(10, 10) await this.page.mouse.move(10, 10)
await this.nextFrame() await this.nextFrame()
} }
async clickContextMenuItem(name: string): Promise<void> {
await this.page.getByRole('menuitem', { name }).click()
}
async select2Nodes() { async select2Nodes() {
// Select 2 CLIP nodes. // Select 2 CLIP nodes.
await this.page.keyboard.down('Control') await this.page.keyboard.down('Control')
@@ -737,19 +787,6 @@ export class ComfyPage {
) )
} }
async confirmDialog(prompt: string, text: string = 'Yes') {
const modal = this.page.locator(
`.comfy-modal-content:has-text("${prompt}")`
)
await expect(modal).toBeVisible()
await modal
.locator('.comfyui-button', {
hasText: text
})
.click()
await expect(modal).toBeHidden()
}
async convertAllNodesToGroupNode(groupNodeName: string) { async convertAllNodesToGroupNode(groupNodeName: string) {
this.page.on('dialog', async (dialog) => { this.page.on('dialog', async (dialog) => {
await dialog.accept(groupNodeName) await dialog.accept(groupNodeName)

View File

@@ -43,7 +43,7 @@ export class ComfyNodeSearchBox {
} }
get filterButton() { get filterButton() {
return this.page.locator('.comfy-vue-node-search-container .filter-button') return this.page.locator('.comfy-vue-node-search-container ._filter-button')
} }
async fillAndSelectFirstNode( async fillAndSelectFirstNode(

View File

@@ -103,12 +103,6 @@ export class WorkflowsSidebarTab extends SidebarTab {
.allInnerTexts() .allInnerTexts()
} }
async getActiveWorkflowName() {
return await this.page
.locator('.comfyui-workflows-open .p-tree-node-selected .node-label')
.innerText()
}
async getTopLevelSavedWorkflowNames() { async getTopLevelSavedWorkflowNames() {
return await this.page return await this.page
.locator('.comfyui-workflows-browse .node-label') .locator('.comfyui-workflows-browse .node-label')

View File

@@ -77,13 +77,8 @@ test.describe('Group Node', () => {
.click() .click()
}) })
}) })
// The 500ms fixed delay on the search results is causing flakiness
// Potential solution: add a spinner state when the search is in progress, test('Can be added to canvas using search', async ({ comfyPage }) => {
// and observe that state from the test. Blocker: the PrimeVue AutoComplete
// does not have a v-model on the query, so we cannot observe the raw
// query update, and thus cannot set the spinning state between the raw query
// update and the debounced search update.
test.skip('Can be added to canvas using search', async ({ comfyPage }) => {
const groupNodeName = 'DefautWorkflowGroupNode' const groupNodeName = 'DefautWorkflowGroupNode'
await comfyPage.convertAllNodesToGroupNode(groupNodeName) await comfyPage.convertAllNodesToGroupNode(groupNodeName)
await comfyPage.doubleClickCanvas() await comfyPage.doubleClickCanvas()

View File

@@ -537,34 +537,6 @@ test.describe('Load workflow', () => {
await comfyPage.loadWorkflow('string_input') await comfyPage.loadWorkflow('string_input')
await expect(comfyPage.canvas).toHaveScreenshot('string_input.png') await expect(comfyPage.canvas).toHaveScreenshot('string_input.png')
}) })
test('Restore workflow on reload (switch workflow)', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('single_ksampler')
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
await comfyPage.reload({ clearStorage: false })
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
})
test('Restore workflow on reload (modify workflow)', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('single_ksampler')
const node = (await comfyPage.getFirstNodeRef())!
await node.click('collapse')
// Wait 300ms between 2 clicks so that it is not treated as a double click
// by litegraph.
await comfyPage.page.waitForTimeout(300)
await comfyPage.clickEmptySpace()
await expect(comfyPage.canvas).toHaveScreenshot(
'single_ksampler_modified.png'
)
await comfyPage.reload({ clearStorage: false })
await expect(comfyPage.canvas).toHaveScreenshot(
'single_ksampler_modified.png'
)
})
}) })
test.describe('Load duplicate workflow', () => { test.describe('Load duplicate workflow', () => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -379,9 +379,7 @@ test.describe('Menu', () => {
// Open the sidebar // Open the sidebar
const tab = comfyPage.menu.workflowsTab const tab = comfyPage.menu.workflowsTab
await tab.open() await tab.open()
})
test.afterEach(async ({ comfyPage }) => {
await comfyPage.setupWorkflowsDirectory({}) await comfyPage.setupWorkflowsDirectory({})
}) })
@@ -452,43 +450,6 @@ test.describe('Menu', () => {
).toEqual(['*Unsaved Workflow.json', 'workflow3.json', 'workflow4.json']) ).toEqual(['*Unsaved Workflow.json', 'workflow3.json', 'workflow4.json'])
}) })
test('Can save workflow as with same name', async ({ comfyPage }) => {
await comfyPage.menu.topbar.saveWorkflow('workflow5.json')
expect(
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
).toEqual(['workflow5.json'])
await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json')
await comfyPage.confirmDialog('Overwrite existing file?', 'Yes')
expect(
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
).toEqual(['workflow5.json'])
})
test('Can overwrite other workflows with save as', async ({
comfyPage
}) => {
const topbar = comfyPage.menu.topbar
await topbar.saveWorkflow('workflow1.json')
await topbar.saveWorkflowAs('workflow2.json')
expect(
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
).toEqual(['workflow1.json', 'workflow2.json'])
expect(await comfyPage.menu.workflowsTab.getActiveWorkflowName()).toEqual(
'workflow2.json'
)
await topbar.saveWorkflowAs('workflow1.json')
await comfyPage.confirmDialog('Overwrite existing file?', 'Yes')
// The old workflow1.json should be deleted and the new one should be saved.
expect(
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
).toEqual(['workflow2.json', 'workflow1.json'])
expect(await comfyPage.menu.workflowsTab.getActiveWorkflowName()).toEqual(
'workflow1.json'
)
})
test('Does not report warning when switching between opened workflows', async ({ test('Does not report warning when switching between opened workflows', async ({
comfyPage comfyPage
}) => { }) => {
@@ -514,7 +475,7 @@ test.describe('Menu', () => {
`tempWorkflow-${test.info().title}` `tempWorkflow-${test.info().title}`
) )
const closeButton = comfyPage.page.locator( const closeButton = comfyPage.page.locator(
'.comfyui-workflows-open .close-workflow-button' '.comfyui-workflows-open .p-button-icon.pi-times'
) )
await closeButton.click() await closeButton.click()
expect( expect(

View File

@@ -0,0 +1,70 @@
import {
comfyPageFixture as test,
comfyExpect as expect
} from './fixtures/ComfyPage'
// Old `nodeTemplate.ts` system
test.describe('Node Template', () => {
test.afterEach(async ({ comfyPage }) => {
await comfyPage.clearNodeTemplates()
})
test('Can create and use node template', async ({ comfyPage }) => {
const templateName = 'Can create node template template'
await comfyPage.clearNodeTemplates()
await comfyPage.reload()
// TODO: Flaky test. Right click requires delay after reload, but other interactions do not.
await comfyPage.page.waitForTimeout(500)
// Enter filename when prompt dialog shown
comfyPage.page.on('dialog', (dialog) => dialog.accept(templateName))
// Ctrl + drag over 3 nodes
await comfyPage.dragAndDrop(
{ x: 175, y: 252 },
{ x: 483, y: 564 },
'ControlOrMeta'
)
expect(await comfyPage.getGraphSelectedItemsCount()).toEqual(3)
await comfyPage.rightClickCanvas()
await comfyPage.clickContextMenuItem('Save Selected as Template')
await comfyPage.nextFrame()
await comfyPage.rightClickCanvas()
await comfyPage.clickContextMenuItem('Node Templates >')
await comfyPage.clickContextMenuItem(templateName)
await expect(comfyPage.canvas).toHaveScreenshot()
})
test('Can load old format template', async ({ comfyPage }) => {
await comfyPage.setNodeTemplates('vintage_clipboard_template.json')
await comfyPage.reload()
// TODO: Flaky test. Right click requires delay after reload, but other interactions do not.
await comfyPage.page.waitForTimeout(500)
await comfyPage.rightClickCanvas()
await comfyPage.clickContextMenuItem('Node Templates >')
await comfyPage.clickContextMenuItem('vintageClipboard Template')
await expect(comfyPage.canvas).toHaveScreenshot()
})
test('Can load new format template', async ({ comfyPage }) => {
await comfyPage.setNodeTemplates('node_template_templates.json')
await comfyPage.reload()
// TODO: Flaky test. Right click requires delay after reload, but other interactions do not.
await comfyPage.page.waitForTimeout(500)
await comfyPage.rightClickCanvas()
await comfyPage.clickContextMenuItem('Node Templates >')
await comfyPage.clickContextMenuItem('Three Nodes Template')
await expect(comfyPage.canvas).toHaveScreenshot()
})
})

View File

@@ -36,7 +36,7 @@ test.describe('Canvas Right Click Menu', () => {
await dialog.accept('GroupNode2CLIP') await dialog.accept('GroupNode2CLIP')
}) })
await comfyPage.rightClickCanvas() await comfyPage.rightClickCanvas()
await comfyPage.clickContextMenuItem('Convert to Group Node') await comfyPage.page.getByText('Convert to Group Node').click()
await comfyPage.nextFrame() await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-node-group-node.png' 'right-click-node-group-node.png'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -14,7 +14,6 @@ const jestConfig: JestConfigWithTsJest = {
} }
] ]
}, },
transformIgnorePatterns: ['/node_modules/(?!(three|@three)/)'],
moduleNameMapper: { moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1', '^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|less|scss|sass)$': 'identity-obj-proxy' '\\.(css|less|scss|sass)$': 'identity-obj-proxy'

View File

@@ -1,13 +0,0 @@
export default {
'./**/*.js': (stagedFiles) => formatFiles(stagedFiles),
'./**/*.{ts,tsx,vue}': (stagedFiles) => [
...formatFiles(stagedFiles),
'tsc --noEmit',
'tsc-strict'
]
}
function formatFiles(fileNames) {
return [`prettier --write ${fileNames.join(' ')}`]
}

111
package-lock.json generated
View File

@@ -1,16 +1,16 @@
{ {
"name": "comfyui-frontend", "name": "comfyui-frontend",
"version": "1.4.4", "version": "1.3.43",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "comfyui-frontend", "name": "comfyui-frontend",
"version": "1.4.4", "version": "1.3.43",
"dependencies": { "dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1", "@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.2.16", "@comfyorg/comfyui-electron-types": "^0.2.16",
"@comfyorg/litegraph": "^0.8.30", "@comfyorg/litegraph": "^0.8.26",
"@primevue/themes": "^4.0.5", "@primevue/themes": "^4.0.5",
"@vueuse/core": "^11.0.0", "@vueuse/core": "^11.0.0",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
@@ -24,7 +24,6 @@
"pinia": "^2.1.7", "pinia": "^2.1.7",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primevue": "^4.0.5", "primevue": "^4.0.5",
"three": "^0.170.0",
"vue": "^3.4.31", "vue": "^3.4.31",
"vue-i18n": "^9.13.1", "vue-i18n": "^9.13.1",
"vue-router": "^4.4.3", "vue-router": "^4.4.3",
@@ -41,7 +40,6 @@
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/lodash": "^4.17.6", "@types/lodash": "^4.17.6",
"@types/node": "^20.14.8", "@types/node": "^20.14.8",
"@types/three": "^0.169.0",
"@vitejs/plugin-vue": "^5.1.4", "@vitejs/plugin-vue": "^5.1.4",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"@vue/vue3-jest": "^29.2.6", "@vue/vue3-jest": "^29.2.6",
@@ -65,6 +63,7 @@
"tailwindcss": "^3.4.4", "tailwindcss": "^3.4.4",
"ts-jest": "^29.1.4", "ts-jest": "^29.1.4",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsc-files": "^1.1.4",
"tsx": "^4.15.6", "tsx": "^4.15.6",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"typescript-eslint": "^8.0.0", "typescript-eslint": "^8.0.0",
@@ -1923,9 +1922,9 @@
"license": "GPL-3.0-only" "license": "GPL-3.0-only"
}, },
"node_modules/@comfyorg/litegraph": { "node_modules/@comfyorg/litegraph": {
"version": "0.8.30", "version": "0.8.26",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.30.tgz", "resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.26.tgz",
"integrity": "sha512-Rzt2PdbPyJD+/zkYrW3ETZzH/NhjKxwTH9kjbCDxUeLHldQ/R8EQTmbvzV3WkrBuYV9nG8RZExDO0Z7K8PBjpA==", "integrity": "sha512-q0Vcd5usphR5nghfyFksVx+VM+eSB1MyX8Ne304KFDnr214KQMA6DAjrEQJlGBUUCybLiOtPCvd3dxPecEQiSQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@cspotcode/source-map-support": { "node_modules/@cspotcode/source-map-support": {
@@ -2460,9 +2459,9 @@
} }
}, },
"node_modules/@eslint/plugin-kit": { "node_modules/@eslint/plugin-kit": {
"version": "0.2.3", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz",
"integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@@ -3851,13 +3850,6 @@
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"dev": true "dev": true
}, },
"node_modules/@tweenjs/tween.js": {
"version": "23.1.3",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/babel__core": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -3993,13 +3985,6 @@
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
"dev": true "dev": true
}, },
"node_modules/@types/stats.js": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz",
"integrity": "sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/strip-bom": { "node_modules/@types/strip-bom": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz",
@@ -4014,21 +3999,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/three": {
"version": "0.169.0",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.169.0.tgz",
"integrity": "sha512-oan7qCgJBt03wIaK+4xPWclYRPG9wzcg7Z2f5T8xYTNEF95kh0t0lklxLLYBDo7gQiGLYzE6iF4ta7nXF2bcsw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@tweenjs/tween.js": "~23.1.3",
"@types/stats.js": "*",
"@types/webxr": "*",
"@webgpu/types": "*",
"fflate": "~0.8.2",
"meshoptimizer": "~0.18.1"
}
},
"node_modules/@types/tough-cookie": { "node_modules/@types/tough-cookie": {
"version": "4.0.5", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
@@ -4041,13 +4011,6 @@
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/webxr": {
"version": "0.5.20",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.20.tgz",
"integrity": "sha512-JGpU6qiIJQKUuVSKx1GtQnHJGxRjtfGIhzO2ilq43VZZS//f1h1Sgexbdk+Lq+7569a6EYhOWrUpIruR/1Enmg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/yargs": { "node_modules/@types/yargs": {
"version": "17.0.32", "version": "17.0.32",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
@@ -4673,13 +4636,6 @@
} }
} }
}, },
"node_modules/@webgpu/types": {
"version": "0.1.51",
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.51.tgz",
"integrity": "sha512-ktR3u64NPjwIViNCck+z9QeyN0iPkQCUOQ07ZCV1RzlkfP+olLTeEZ95O1QHS+v4w9vJeY9xj/uJuSphsHy5rQ==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@xterm/addon-fit": { "node_modules/@xterm/addon-fit": {
"version": "0.10.0", "version": "0.10.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
@@ -5851,11 +5807,10 @@
"dev": true "dev": true
}, },
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"path-key": "^3.1.0", "path-key": "^3.1.0",
"shebang-command": "^2.0.0", "shebang-command": "^2.0.0",
@@ -6823,13 +6778,6 @@
"bser": "2.1.1" "bser": "2.1.1"
} }
}, },
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"dev": true,
"license": "MIT"
},
"node_modules/file-entry-cache": { "node_modules/file-entry-cache": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -10119,19 +10067,11 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/meshoptimizer": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
"integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
"dev": true,
"license": "MIT"
},
"node_modules/micromatch": { "node_modules/micromatch": {
"version": "4.0.8", "version": "4.0.7",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"braces": "^3.0.3", "braces": "^3.0.3",
"picomatch": "^2.3.1" "picomatch": "^2.3.1"
@@ -12012,12 +11952,6 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/three": {
"version": "0.170.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz",
"integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==",
"license": "MIT"
},
"node_modules/tinybench": { "node_modules/tinybench": {
"version": "2.9.0", "version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -12236,6 +12170,19 @@
} }
} }
}, },
"node_modules/tsc-files": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/tsc-files/-/tsc-files-1.1.4.tgz",
"integrity": "sha512-RePsRsOLru3BPpnf237y1Xe1oCGta8rmSYzM76kYo5tLGsv5R2r3s64yapYorGTPuuLyfS9NVbh9ydzmvNie2w==",
"dev": true,
"license": "MIT",
"bin": {
"tsc-files": "cli.js"
},
"peerDependencies": {
"typescript": ">=3"
}
},
"node_modules/tsconfig": { "node_modules/tsconfig": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz",

View File

@@ -1,7 +1,7 @@
{ {
"name": "comfyui-frontend", "name": "comfyui-frontend",
"private": true, "private": true,
"version": "1.4.4", "version": "1.3.43",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -35,7 +35,6 @@
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/lodash": "^4.17.6", "@types/lodash": "^4.17.6",
"@types/node": "^20.14.8", "@types/node": "^20.14.8",
"@types/three": "^0.169.0",
"@vitejs/plugin-vue": "^5.1.4", "@vitejs/plugin-vue": "^5.1.4",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"@vue/vue3-jest": "^29.2.6", "@vue/vue3-jest": "^29.2.6",
@@ -59,6 +58,7 @@
"tailwindcss": "^3.4.4", "tailwindcss": "^3.4.4",
"ts-jest": "^29.1.4", "ts-jest": "^29.1.4",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsc-files": "^1.1.4",
"tsx": "^4.15.6", "tsx": "^4.15.6",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"typescript-eslint": "^8.0.0", "typescript-eslint": "^8.0.0",
@@ -73,7 +73,7 @@
"dependencies": { "dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1", "@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.2.16", "@comfyorg/comfyui-electron-types": "^0.2.16",
"@comfyorg/litegraph": "^0.8.30", "@comfyorg/litegraph": "^0.8.26",
"@primevue/themes": "^4.0.5", "@primevue/themes": "^4.0.5",
"@vueuse/core": "^11.0.0", "@vueuse/core": "^11.0.0",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
@@ -87,11 +87,14 @@
"pinia": "^2.1.7", "pinia": "^2.1.7",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primevue": "^4.0.5", "primevue": "^4.0.5",
"three": "^0.170.0",
"vue": "^3.4.31", "vue": "^3.4.31",
"vue-i18n": "^9.13.1", "vue-i18n": "^9.13.1",
"vue-router": "^4.4.3", "vue-router": "^4.4.3",
"zod": "^3.23.8", "zod": "^3.23.8",
"zod-validation-error": "^3.3.0" "zod-validation-error": "^3.3.0"
},
"lint-staged": {
"./**/*.{js,ts,tsx,vue}": "prettier --write",
"**/*.ts": "tsc-files --noEmit"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 410 B

View File

@@ -125,45 +125,30 @@ const adjustMenuPosition = () => {
const menuWidth = panelRef.value.offsetWidth const menuWidth = panelRef.value.offsetWidth
const menuHeight = panelRef.value.offsetHeight const menuHeight = panelRef.value.offsetHeight
// Calculate distances to all edges // Calculate the distance from each edge
const distanceLeft = lastDragState.value.x
const distanceRight = const distanceRight =
lastDragState.value.windowWidth - (lastDragState.value.x + menuWidth) lastDragState.value.windowWidth - (lastDragState.value.x + menuWidth)
const distanceTop = lastDragState.value.y
const distanceBottom = const distanceBottom =
lastDragState.value.windowHeight - (lastDragState.value.y + menuHeight) lastDragState.value.windowHeight - (lastDragState.value.y + menuHeight)
// Find the smallest distance to determine which edge to anchor to // Determine if the menu is closer to right/bottom or left/top
const distances = [ const anchorRight = distanceRight < lastDragState.value.x
{ edge: 'left', distance: distanceLeft }, const anchorBottom = distanceBottom < lastDragState.value.y
{ edge: 'right', distance: distanceRight },
{ edge: 'top', distance: distanceTop },
{ edge: 'bottom', distance: distanceBottom }
]
const closestEdge = distances.reduce((min, curr) =>
curr.distance < min.distance ? curr : min
)
// Calculate vertical position as a percentage of screen height // Calculate new position
const verticalRatio = if (anchorRight) {
lastDragState.value.y / lastDragState.value.windowHeight x.value =
const horizontalRatio = screenWidth - (lastDragState.value.windowWidth - lastDragState.value.x)
lastDragState.value.x / lastDragState.value.windowWidth
// Apply positioning based on closest edge
if (closestEdge.edge === 'left') {
x.value = closestEdge.distance // Maintain exact distance from left
y.value = verticalRatio * screenHeight
} else if (closestEdge.edge === 'right') {
x.value = screenWidth - menuWidth - closestEdge.distance // Maintain exact distance from right
y.value = verticalRatio * screenHeight
} else if (closestEdge.edge === 'top') {
x.value = horizontalRatio * screenWidth
y.value = closestEdge.distance // Maintain exact distance from top
} else { } else {
// bottom x.value = lastDragState.value.x
x.value = horizontalRatio * screenWidth }
y.value = screenHeight - menuHeight - closestEdge.distance // Maintain exact distance from bottom
if (anchorBottom) {
y.value =
screenHeight -
(lastDragState.value.windowHeight - lastDragState.value.y)
} else {
y.value = lastDragState.value.y
} }
// Ensure the menu stays within the screen bounds // Ensure the menu stays within the screen bounds

View File

@@ -0,0 +1,104 @@
<template>
<div class="relative h-full w-full bg-black">
<p v-if="errorMessage" class="p-4 text-center">{{ errorMessage }}</p>
<ProgressSpinner
v-else-if="loading"
class="absolute inset-0 flex justify-center items-center h-full z-10"
/>
<div v-show="!loading" class="p-terminal rounded-none h-full w-full p-2">
<div class="h-full" ref="terminalEl"></div>
</div>
</div>
</template>
<script setup lang="ts">
import '@xterm/xterm/css/xterm.css'
import { Terminal } from '@xterm/xterm'
import { FitAddon } from '@xterm/addon-fit'
import { api } from '@/scripts/api'
import { onMounted, onUnmounted, ref } from 'vue'
import { debounce } from 'lodash'
import ProgressSpinner from 'primevue/progressspinner'
import { useExecutionStore } from '@/stores/executionStore'
import { storeToRefs } from 'pinia'
import { until } from '@vueuse/core'
import { LogEntry, LogsWsMessage, TerminalSize } from '@/types/apiTypes'
const errorMessage = ref('')
const loading = ref(true)
const terminalEl = ref<HTMLDivElement>()
const fitAddon = new FitAddon()
const terminal = new Terminal({
convertEol: true
})
terminal.loadAddon(fitAddon)
const resizeTerminal = () =>
terminal.resize(terminal.cols, fitAddon.proposeDimensions().rows)
const resizeObserver = new ResizeObserver(debounce(resizeTerminal, 50))
const update = (entries: Array<LogEntry>, size?: TerminalSize) => {
if (size) {
terminal.resize(size.cols, fitAddon.proposeDimensions().rows)
}
terminal.write(entries.map((e) => e.m).join(''))
}
const logReceived = (e: CustomEvent<LogsWsMessage>) => {
update(e.detail.entries, e.detail.size)
}
const loadLogEntries = async () => {
const logs = await api.getRawLogs()
update(logs.entries, logs.size)
}
const watchLogs = async () => {
const { clientId } = storeToRefs(useExecutionStore())
if (!clientId.value) {
await until(clientId).not.toBeNull()
}
api.subscribeLogs(true)
api.addEventListener('logs', logReceived)
}
onMounted(async () => {
terminal.open(terminalEl.value)
try {
await loadLogEntries()
} catch (err) {
console.error('Error loading logs', err)
// On older backends the endpoints wont exist
errorMessage.value =
'Unable to load logs, please ensure you have updated your ComfyUI backend.'
return
}
loading.value = false
resizeObserver.observe(terminalEl.value)
await watchLogs()
})
onUnmounted(() => {
if (api.clientId) {
api.subscribeLogs(false)
}
api.removeEventListener('logs', logReceived)
resizeObserver.disconnect()
})
</script>
<style scoped>
:deep(.p-terminal) .xterm {
overflow-x: auto;
}
:deep(.p-terminal) .xterm-screen {
background-color: black;
overflow-y: hidden;
}
</style>

View File

@@ -1,30 +0,0 @@
<template>
<div class="relative h-full w-full bg-black" ref="rootEl">
<div class="p-terminal rounded-none h-full w-full p-2">
<div class="h-full terminal-host" ref="terminalEl"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, Ref } from 'vue'
import { useTerminal } from '@/hooks/bottomPanelTabs/useTerminal'
const emit = defineEmits<{
created: [ReturnType<typeof useTerminal>, Ref<HTMLElement>]
}>()
const terminalEl = ref<HTMLElement>()
const rootEl = ref<HTMLElement>()
emit('created', useTerminal(terminalEl), rootEl)
</script>
<style scoped>
:deep(.p-terminal) .xterm {
overflow-x: auto;
}
:deep(.p-terminal) .xterm-screen {
background-color: black;
overflow-y: hidden;
}
</style>

View File

@@ -1,73 +0,0 @@
<template>
<BaseTerminal @created="terminalCreated" />
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, Ref } from 'vue'
import type { useTerminal } from '@/hooks/bottomPanelTabs/useTerminal'
import { electronAPI } from '@/utils/envUtil'
import { IDisposable } from '@xterm/xterm'
import BaseTerminal from './BaseTerminal.vue'
const terminalCreated = (
{ terminal, useAutoSize }: ReturnType<typeof useTerminal>,
root: Ref<HTMLElement>
) => {
// TODO: use types from electron package
const terminalApi = electronAPI()['Terminal'] as {
onOutput(cb: (message: string) => void): () => void
resize(cols: number, rows: number): void
restore(): Promise<{
buffer: string[]
pos: { x: number; y: number }
size: { cols: number; rows: number }
}>
storePos(x: number, y: number): void
write(data: string): void
}
let offData: IDisposable
let offOutput: () => void
useAutoSize(root, true, true, () => {
// If we aren't visible, don't resize
if (!terminal.element?.offsetParent) return
terminalApi.resize(terminal.cols, terminal.rows)
})
onMounted(async () => {
offData = terminal.onData(async (message: string) => {
terminalApi.write(message)
})
offOutput = terminalApi.onOutput((message) => {
terminal.write(message)
})
const restore = await terminalApi.restore()
setTimeout(() => {
if (restore.buffer.length) {
terminal.resize(restore.size.cols, restore.size.rows)
terminal.write(restore.buffer.join(''))
}
}, 500)
})
onUnmounted(() => {
offData?.dispose()
offOutput?.()
})
}
</script>
<style scoped>
:deep(.p-terminal) .xterm {
overflow-x: auto;
}
:deep(.p-terminal) .xterm-screen {
background-color: black;
overflow-y: hidden;
}
</style>

View File

@@ -1,90 +0,0 @@
<template>
<div class="bg-black h-full w-full">
<p v-if="errorMessage" class="p-4 text-center">{{ errorMessage }}</p>
<ProgressSpinner
v-else-if="loading"
class="relative inset-0 flex justify-center items-center h-full z-10"
/>
<BaseTerminal v-show="!loading" @created="terminalCreated" />
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, Ref, ref } from 'vue'
import type { useTerminal } from '@/hooks/bottomPanelTabs/useTerminal'
import { LogEntry, LogsWsMessage, TerminalSize } from '@/types/apiTypes'
import { api } from '@/scripts/api'
import { useExecutionStore } from '@/stores/executionStore'
import { until } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import BaseTerminal from './BaseTerminal.vue'
import ProgressSpinner from 'primevue/progressspinner'
const errorMessage = ref('')
const loading = ref(true)
const terminalCreated = (
{ terminal, useAutoSize }: ReturnType<typeof useTerminal>,
root: Ref<HTMLElement>
) => {
useAutoSize(root, true, false)
const update = (entries: Array<LogEntry>, size?: TerminalSize) => {
if (size) {
terminal.resize(size.cols, terminal.rows)
}
terminal.write(entries.map((e) => e.m).join(''))
}
const logReceived = (e: CustomEvent<LogsWsMessage>) => {
update(e.detail.entries, e.detail.size)
}
const loadLogEntries = async () => {
const logs = await api.getRawLogs()
update(logs.entries, logs.size)
}
const watchLogs = async () => {
const { clientId } = storeToRefs(useExecutionStore())
if (!clientId.value) {
await until(clientId).not.toBeNull()
}
api.subscribeLogs(true)
api.addEventListener('logs', logReceived)
}
onMounted(async () => {
try {
await loadLogEntries()
} catch (err) {
console.error('Error loading logs', err)
// On older backends the endpoints wont exist
errorMessage.value =
'Unable to load logs, please ensure you have updated your ComfyUI backend.'
return
}
await watchLogs()
loading.value = false
})
onUnmounted(() => {
if (api.clientId) {
api.subscribeLogs(false)
}
api.removeEventListener('logs', logReceived)
})
}
</script>
<style scoped>
:deep(.p-terminal) .xterm {
overflow-x: auto;
}
:deep(.p-terminal) .xterm-screen {
background-color: black;
overflow-y: hidden;
}
</style>

View File

@@ -28,14 +28,7 @@
class="flex flex-row items-center gap-2" class="flex flex-row items-center gap-2"
v-if="status === 'in_progress' || status === 'paused'" v-if="status === 'in_progress' || status === 'paused'"
> >
<!-- Temporary fix for issue when % only comes into view only if the progress bar is large enough <ProgressBar class="flex-1" :value="downloadProgress" />
https://comfy-organization.slack.com/archives/C07H3GLKDPF/p1731551013385499
-->
<ProgressBar
class="flex-1"
:value="downloadProgress"
:show-value="downloadProgress > 10"
/>
<Button <Button
class="file-action-button" class="file-action-button"

View File

@@ -13,7 +13,6 @@
:modelValue="modelValue" :modelValue="modelValue"
@update:modelValue="updateValue" @update:modelValue="updateValue"
class="input-part" class="input-part"
:max-fraction-digits="3"
:class="inputClass" :class="inputClass"
:min="min" :min="min"
:max="max" :max="max"

View File

@@ -1,6 +1,6 @@
<template> <template>
<Tree <Tree
class="tree-explorer py-0 px-2 2xl:px-4" class="tree-explorer p-2 2xl:p-4"
:class="props.class" :class="props.class"
v-model:expandedKeys="expandedKeys" v-model:expandedKeys="expandedKeys"
v-model:selectionKeys="selectionKeys" v-model:selectionKeys="selectionKeys"

View File

@@ -47,8 +47,7 @@ import {
DragAndScale, DragAndScale,
LGraphCanvas, LGraphCanvas,
ContextMenu, ContextMenu,
LGraphBadge, LGraphBadge
CanvasPointer
} from '@comfyorg/litegraph' } from '@comfyorg/litegraph'
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes' import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
import { useCanvasStore } from '@/stores/graphStore' import { useCanvasStore } from '@/stores/graphStore'
@@ -62,7 +61,6 @@ import { usePragmaticDroppable } from '@/hooks/dndHooks'
import { useWorkflowStore } from '@/stores/workflowStore' import { useWorkflowStore } from '@/stores/workflowStore'
import { setStorageValue } from '@/scripts/utils' import { setStorageValue } from '@/scripts/utils'
import { ChangeTracker } from '@/scripts/changeTracker' import { ChangeTracker } from '@/scripts/changeTracker'
import { api } from '@/scripts/api'
const emit = defineEmits(['ready']) const emit = defineEmits(['ready'])
const canvasRef = ref<HTMLCanvasElement | null>(null) const canvasRef = ref<HTMLCanvasElement | null>(null)
@@ -163,31 +161,6 @@ watchEffect(() => {
} }
}) })
watchEffect(() => {
CanvasPointer.doubleClickTime = settingStore.get(
'Comfy.Pointer.DoubleClickTime'
)
})
watchEffect(() => {
CanvasPointer.bufferTime = settingStore.get('Comfy.Pointer.ClickBufferTime')
})
watchEffect(() => {
CanvasPointer.maxClickDrift = settingStore.get('Comfy.Pointer.ClickDrift')
})
watchEffect(() => {
LiteGraph.CANVAS_GRID_SIZE = settingStore.get('Comfy.SnapToGrid.GridSize')
})
watchEffect(() => {
const alwaysSnapToGrid = settingStore.get('pysssss.SnapToGrid')
if (comfyApp.graph?.config) {
comfyApp.graph.config.alwaysSnapToGrid = alwaysSnapToGrid
}
})
watchEffect(() => { watchEffect(() => {
if (!canvasStore.canvas) return if (!canvasStore.canvas) return
@@ -205,26 +178,13 @@ watchEffect(() => {
}) })
const workflowStore = useWorkflowStore() const workflowStore = useWorkflowStore()
const persistCurrentWorkflow = () => {
const workflow = JSON.stringify(comfyApp.serializeGraph())
localStorage.setItem('workflow', workflow)
if (api.clientId) {
sessionStorage.setItem(`workflow:${api.clientId}`, workflow)
}
}
watchEffect(() => { watchEffect(() => {
if (workflowStore.activeWorkflow) { if (workflowStore.activeWorkflow) {
const workflow = workflowStore.activeWorkflow const workflow = workflowStore.activeWorkflow
setStorageValue('Comfy.PreviousWorkflow', workflow.key) setStorageValue('Comfy.PreviousWorkflow', workflow.key)
// When the activeWorkflow changes, the graph has already been loaded.
// Saving the current state of the graph to the localStorage.
persistCurrentWorkflow()
} }
}) })
api.addEventListener('graphChanged', persistCurrentWorkflow)
usePragmaticDroppable(() => canvasRef.value, { usePragmaticDroppable(() => canvasRef.value, {
onDrop: (event) => { onDrop: (event) => {
const loc = event.location.current.input const loc = event.location.current.input
@@ -302,7 +262,6 @@ onMounted(async () => {
ChangeTracker.init(comfyApp) ChangeTracker.init(comfyApp)
await comfyApp.setup(canvasRef.value) await comfyApp.setup(canvasRef.value)
canvasStore.canvas = comfyApp.canvas canvasStore.canvas = comfyApp.canvas
canvasStore.canvas.render_canvas_border = false
workspaceStore.spinner = false workspaceStore.spinner = false
window['app'] = comfyApp window['app'] = comfyApp

View File

@@ -85,14 +85,12 @@ onMounted(async () => {
appData.value = paths.appData appData.value = paths.appData
appPath.value = paths.appPath appPath.value = paths.appPath
installPath.value = paths.defaultInstallPath installPath.value = paths.defaultInstallPath
await validatePath(paths.defaultInstallPath)
}) })
const validatePath = async (path: string) => { const validatePath = async () => {
try { try {
pathError.value = '' pathError.value = ''
const validation = await electron.validateInstallPath(path) const validation = await electron.validateInstallPath(installPath.value)
if (!validation.isValid) { if (!validation.isValid) {
pathError.value = validation.error pathError.value = validation.error
@@ -107,7 +105,7 @@ const browsePath = async () => {
const result = await electron.showDirectoryPicker() const result = await electron.showDirectoryPicker()
if (result) { if (result) {
installPath.value = result installPath.value = result
await validatePath(result) await validatePath()
} }
} catch (error) { } catch (error) {
pathError.value = t('install.failedToSelectDirectory') pathError.value = t('install.failedToSelectDirectory')

View File

@@ -1,11 +1,6 @@
<template> <template>
<div <div class="comfy-vue-node-search-container">
class="comfy-vue-node-search-container flex justify-center items-center w-full min-w-96 pointer-events-auto" <div class="comfy-vue-node-preview-container" v-if="enableNodePreview">
>
<div
class="comfy-vue-node-preview-container absolute left-[-350px] top-[50px]"
v-if="enableNodePreview"
>
<NodePreview <NodePreview
:nodeDef="hoveredSuggestion" :nodeDef="hoveredSuggestion"
:key="hoveredSuggestion?.name || ''" :key="hoveredSuggestion?.name || ''"
@@ -16,10 +11,10 @@
<Button <Button
icon="pi pi-filter" icon="pi pi-filter"
severity="secondary" severity="secondary"
class="filter-button z-10" class="_filter-button"
@click="nodeSearchFilterVisible = true" @click="nodeSearchFilterVisible = true"
/> />
<Dialog v-model:visible="nodeSearchFilterVisible" class="min-w-96"> <Dialog v-model:visible="nodeSearchFilterVisible" class="_dialog">
<template #header> <template #header>
<h3>Add node filter condition</h3> <h3>Add node filter condition</h3>
</template> </template>
@@ -30,7 +25,7 @@
<AutoCompletePlus <AutoCompletePlus
:model-value="props.filters" :model-value="props.filters"
class="comfy-vue-node-search-box z-10 flex-grow" class="comfy-vue-node-search-box"
scrollHeight="40vh" scrollHeight="40vh"
:placeholder="placeholder" :placeholder="placeholder"
:input-id="inputId" :input-id="inputId"
@@ -153,3 +148,31 @@ const setHoverSuggestion = (index: number) => {
hoveredSuggestion.value = value hoveredSuggestion.value = value
} }
</script> </script>
<style scoped>
.comfy-vue-node-search-container {
@apply flex justify-center items-center w-full min-w-96;
}
.comfy-vue-node-search-container * {
pointer-events: auto;
}
.comfy-vue-node-preview-container {
position: absolute;
left: -350px;
top: 50px;
}
.comfy-vue-node-search-box {
@apply z-10 flex-grow;
}
._filter-button {
z-index: 10;
}
._dialog {
@apply min-w-96;
}
</style>

View File

@@ -31,7 +31,7 @@
<ElectronDownloadItems v-if="isElectron()" /> <ElectronDownloadItems v-if="isElectron()" />
<TreeExplorer <TreeExplorer
class="model-lib-tree-explorer" class="model-lib-tree-explorer py-0"
:roots="renderedRoot.children" :roots="renderedRoot.children"
v-model:expandedKeys="expandedKeys" v-model:expandedKeys="expandedKeys"
> >

View File

@@ -48,7 +48,7 @@
class="m-2" class="m-2"
/> />
<TreeExplorer <TreeExplorer
class="node-lib-tree-explorer" class="node-lib-tree-explorer py-0"
:roots="renderedRoot.children" :roots="renderedRoot.children"
v-model:expandedKeys="expandedKeys" v-model:expandedKeys="expandedKeys"
> >

View File

@@ -1,18 +1,6 @@
<template> <template>
<SidebarTabTemplate :title="$t('sideToolbar.queue')"> <SidebarTabTemplate :title="$t('sideToolbar.queue')">
<template #tool-buttons> <template #tool-buttons>
<Popover ref="outputFilterPopup">
<OutputFilters />
</Popover>
<Button
icon="pi pi-filter"
text
severity="secondary"
@click="outputFilterPopup.toggle($event)"
v-tooltip="$t(`sideToolbar.queueTab.filter`)"
:class="{ 'text-yellow-500': anyFilter }"
/>
<Button <Button
:icon=" :icon="
imageFit === 'cover' imageFit === 'cover'
@@ -111,7 +99,6 @@ import Button from 'primevue/button'
import ConfirmPopup from 'primevue/confirmpopup' import ConfirmPopup from 'primevue/confirmpopup'
import ContextMenu from 'primevue/contextmenu' import ContextMenu from 'primevue/contextmenu'
import type { MenuItem } from 'primevue/menuitem' import type { MenuItem } from 'primevue/menuitem'
import Popover from 'primevue/popover'
import ProgressSpinner from 'primevue/progressspinner' import ProgressSpinner from 'primevue/progressspinner'
import TaskItem from './queue/TaskItem.vue' import TaskItem from './queue/TaskItem.vue'
import ResultGallery from './queue/ResultGallery.vue' import ResultGallery from './queue/ResultGallery.vue'
@@ -124,9 +111,7 @@ import { useSettingStore } from '@/stores/settingStore'
import { useCommandStore } from '@/stores/commandStore' import { useCommandStore } from '@/stores/commandStore'
import { app } from '@/scripts/app' import { app } from '@/scripts/app'
const SETTING_FIT = 'Comfy.Queue.ImageFit' const IMAGE_FIT = 'Comfy.Queue.ImageFit'
const SETTING_FLAT = 'Comfy.Queue.ShowFlatList'
const SETTING_FILTER = 'Comfy.Queue.Filter'
const confirm = useConfirm() const confirm = useConfirm()
const toast = useToast() const toast = useToast()
const queueStore = useQueueStore() const queueStore = useQueueStore()
@@ -135,7 +120,7 @@ const commandStore = useCommandStore()
const { t } = useI18n() const { t } = useI18n()
// Expanded view: show all outputs in a flat list. // Expanded view: show all outputs in a flat list.
const isExpanded = computed<boolean>(() => settingStore.get(SETTING_FLAT)) const isExpanded = ref(false)
const visibleTasks = ref<TaskItemImpl[]>([]) const visibleTasks = ref<TaskItemImpl[]>([])
const scrollContainer = ref<HTMLElement | null>(null) const scrollContainer = ref<HTMLElement | null>(null)
const loadMoreTrigger = ref<HTMLElement | null>(null) const loadMoreTrigger = ref<HTMLElement | null>(null)
@@ -143,23 +128,7 @@ const galleryActiveIndex = ref(-1)
// Folder view: only show outputs from a single selected task. // Folder view: only show outputs from a single selected task.
const folderTask = ref<TaskItemImpl | null>(null) const folderTask = ref<TaskItemImpl | null>(null)
const isInFolderView = computed(() => folderTask.value !== null) const isInFolderView = computed(() => folderTask.value !== null)
const imageFit = computed<string>(() => settingStore.get(SETTING_FIT)) const imageFit = computed<string>(() => settingStore.get(IMAGE_FIT))
const hideCached = computed<boolean>(
() => settingStore.get(SETTING_FILTER)?.hideCached
)
const hideCanceled = computed<boolean>(
() => settingStore.get(SETTING_FILTER)?.hideCanceled
)
const anyFilter = computed(() => hideCanceled.value || hideCached.value)
watch(hideCached, () => {
updateVisibleTasks()
})
watch(hideCanceled, () => {
updateVisibleTasks()
})
const outputFilterPopup = ref(null)
const ITEMS_PER_PAGE = 8 const ITEMS_PER_PAGE = 8
const SCROLL_THRESHOLD = 100 // pixels from bottom to trigger load const SCROLL_THRESHOLD = 100 // pixels from bottom to trigger load
@@ -180,31 +149,9 @@ const allGalleryItems = computed(() =>
}) })
) )
const filterTasks = (tasks: TaskItemImpl[]) =>
tasks
.filter((t) => {
if (
hideCanceled.value &&
t.status?.messages?.at(-1)?.[0] === 'execution_interrupted'
) {
return false
}
if (
hideCached.value &&
t.flatOutputs?.length &&
t.flatOutputs.every((o) => o.cached)
) {
return false
}
return true
})
.slice(0, ITEMS_PER_PAGE)
const loadMoreItems = () => { const loadMoreItems = () => {
const currentLength = visibleTasks.value.length const currentLength = visibleTasks.value.length
const newTasks = filterTasks(allTasks.value).slice( const newTasks = allTasks.value.slice(
currentLength, currentLength,
currentLength + ITEMS_PER_PAGE currentLength + ITEMS_PER_PAGE
) )
@@ -239,11 +186,11 @@ useResizeObserver(scrollContainer, () => {
}) })
const updateVisibleTasks = () => { const updateVisibleTasks = () => {
visibleTasks.value = filterTasks(allTasks.value) visibleTasks.value = allTasks.value.slice(0, ITEMS_PER_PAGE)
} }
const toggleExpanded = () => { const toggleExpanded = () => {
settingStore.set(SETTING_FLAT, !isExpanded.value) isExpanded.value = !isExpanded.value
updateVisibleTasks() updateVisibleTasks()
} }
@@ -344,10 +291,7 @@ const exitFolderView = () => {
} }
const toggleImageFit = () => { const toggleImageFit = () => {
settingStore.set( settingStore.set(IMAGE_FIT, imageFit.value === 'cover' ? 'contain' : 'cover')
SETTING_FIT,
imageFit.value === 'cover' ? 'contain' : 'cover'
)
} }
onMounted(() => { onMounted(() => {

View File

@@ -54,7 +54,6 @@
</template> </template>
<template #actions="{ node }"> <template #actions="{ node }">
<Button <Button
class="close-workflow-button"
icon="pi pi-times" icon="pi pi-times"
text text
:severity=" :severity="

View File

@@ -16,13 +16,9 @@
class="mt-2 flex flex-row items-center gap-2" class="mt-2 flex flex-row items-center gap-2"
v-if="['in_progress', 'paused', 'completed'].includes(download.status)" v-if="['in_progress', 'paused', 'completed'].includes(download.status)"
> >
<!-- Temporary fix for issue when % only comes into view only if the progress bar is large enough
https://comfy-organization.slack.com/archives/C07H3GLKDPF/p1731551013385499
-->
<ProgressBar <ProgressBar
class="flex-1" class="flex-1"
:value="Number((download.progress * 100).toFixed(1))" :value="Number((download.progress * 100).toFixed(1))"
:show-value="download.progress > 0.1"
/> />
<Button <Button

View File

@@ -1,6 +1,6 @@
<template> <template>
<TreeExplorer <TreeExplorer
class="node-lib-bookmark-tree-explorer" class="node-lib-bookmark-tree-explorer py-0"
ref="treeExplorerRef" ref="treeExplorerRef"
:roots="renderedBookmarkedRoot.children" :roots="renderedBookmarkedRoot.children"
:expandedKeys="expandedKeys" :expandedKeys="expandedKeys"

View File

@@ -1,38 +0,0 @@
<template>
<div class="flex flex-col gap-2">
<label class="flex items-center gap-2">
{{ $t('sideToolbar.queueTab.filters.hideCached') }}
<ToggleSwitch v-model="hideCached" />
</label>
<label class="flex items-center gap-2">
{{ $t('sideToolbar.queueTab.filters.hideCanceled') }}
<ToggleSwitch v-model="hideCanceled" />
</label>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import ToggleSwitch from 'primevue/toggleswitch'
import { useSettingStore } from '@/stores/settingStore'
const SETTING_FILTER = 'Comfy.Queue.Filter'
const { t } = useI18n()
const settingStore = useSettingStore()
const filter = settingStore.get(SETTING_FILTER) ?? {}
const createCompute = (k: string) =>
computed({
get() {
return filter[k]
},
set(value) {
filter[k] = value
settingStore.set(SETTING_FILTER, filter)
}
})
const hideCached = createCompute('hideCached')
const hideCanceled = createCompute('hideCanceled')
</script>

View File

@@ -31,20 +31,16 @@
<div class="task-item-details"> <div class="task-item-details">
<div class="tag-wrapper status-tag-group"> <div class="tag-wrapper status-tag-group">
<Tag v-if="isFlatTask && task.isHistory && node" class="node-name-tag"> <Tag v-if="isFlatTask && task.isHistory" class="node-name-tag">
<Button <Button
class="task-node-link" class="task-node-link"
:label="`${node.type} (#${node.id})`" :label="`${node?.type} (#${node?.id})`"
link link
size="small" size="small"
@click="app.goToNode(node?.id)" @click="app.goToNode(node?.id)"
/> />
</Tag> </Tag>
<Tag <Tag :severity="taskTagSeverity(task.displayStatus)">
:severity="taskTagSeverity(task.displayStatus)"
class="task-duration relative"
>
<i v-if="isCachedResult" class="pi pi-server task-cached-icon"></i>
<span v-html="taskStatusText(task.displayStatus)"></span> <span v-html="taskStatusText(task.displayStatus)"></span>
<span v-if="task.isHistory" class="task-time"> <span v-if="task.isHistory" class="task-time">
{{ formatTime(task.executionTimeInSeconds) }} {{ formatTime(task.executionTimeInSeconds) }}
@@ -94,7 +90,6 @@ const node: ComfyNode | null =
) ?? null ) ?? null
: null : null
const progressPreviewBlobUrl = ref('') const progressPreviewBlobUrl = ref('')
const isCachedResult = props.isFlatTask && coverResult?.cached
const emit = defineEmits<{ const emit = defineEmits<{
( (
@@ -147,7 +142,7 @@ const taskStatusText = (status: TaskItemDisplayStatus) => {
case TaskItemDisplayStatus.Running: case TaskItemDisplayStatus.Running:
return '<i class="pi pi-spin pi-spinner" style="font-weight: bold"></i> Running' return '<i class="pi pi-spin pi-spinner" style="font-weight: bold"></i> Running'
case TaskItemDisplayStatus.Completed: case TaskItemDisplayStatus.Completed:
return `<i class="pi pi-check${isCachedResult ? ' cached' : ''}" style="font-weight: bold"></i>` return '<i class="pi pi-check" style="font-weight: bold"></i>'
case TaskItemDisplayStatus.Failed: case TaskItemDisplayStatus.Failed:
return 'Failed' return 'Failed'
case TaskItemDisplayStatus.Cancelled: case TaskItemDisplayStatus.Cancelled:
@@ -231,15 +226,4 @@ are floating on top of images. */
object-fit: cover; object-fit: cover;
object-position: center; object-position: center;
} }
.task-cached-icon {
color: #fff;
}
:deep(.pi-check.cached) {
font-size: 12px;
position: absolute;
left: 16px;
top: 12px;
}
</style> </style>

View File

@@ -733,7 +733,7 @@ app.registerExtension({
app.ui.settings.addSetting({ app.ui.settings.addSetting({
id, id,
category: ['Appearance', 'ColorPalette'], category: ['Comfy', 'ColorPalette'],
name: 'Color Palette', name: 'Color Palette',
type: (name, setter, value) => { type: (name, setter, value) => {
const options = [ const options = [

View File

@@ -1,153 +0,0 @@
import { app } from '@/scripts/app'
import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
;(async () => {
if (!isElectron()) return
const electronAPI = getElectronAPI()
const desktopAppVersion = await electronAPI.getElectronVersion()
app.registerExtension({
name: 'Comfy.ElectronAdapter',
settings: [
{
id: 'Comfy-Desktop.AutoUpdate',
category: ['Comfy-Desktop', 'General', 'AutoUpdate'],
name: 'Automatically check for updates',
type: 'boolean',
defaultValue: true,
onChange(newValue, oldValue) {
if (oldValue !== undefined && newValue !== oldValue) {
electronAPI.restartApp(
'Restart ComfyUI to apply changes.',
1500 // add delay to allow changes to take effect before restarting.
)
}
}
},
{
id: 'Comfy-Desktop.SendStatistics',
category: ['Comfy-Desktop', 'General', 'Send Statistics'],
name: 'Send anonymous usage statistics',
type: 'boolean',
defaultValue: true,
onChange(newValue, oldValue) {
if (oldValue !== undefined && newValue !== oldValue) {
electronAPI.restartApp(
'Restart ComfyUI to apply changes.',
1500 // add delay to allow changes to take effect before restarting.
)
}
}
}
],
commands: [
{
id: 'Comfy-Desktop.Folders.OpenLogsFolder',
label: 'Open Logs Folder',
icon: 'pi pi-folder-open',
function() {
electronAPI.openLogsFolder()
}
},
{
id: 'Comfy-Desktop.Folders.OpenModelsFolder',
label: 'Open Models Folder',
icon: 'pi pi-folder-open',
function() {
electronAPI.openModelsFolder()
}
},
{
id: 'Comfy-Desktop.Folders.OpenOutputsFolder',
label: 'Open Outputs Folder',
icon: 'pi pi-folder-open',
function() {
electronAPI.openOutputsFolder()
}
},
{
id: 'Comfy-Desktop.Folders.OpenInputsFolder',
label: 'Open Inputs Folder',
icon: 'pi pi-folder-open',
function() {
electronAPI.openInputsFolder()
}
},
{
id: 'Comfy-Desktop.Folders.OpenCustomNodesFolder',
label: 'Open Custom Nodes Folder',
icon: 'pi pi-folder-open',
function() {
electronAPI.openCustomNodesFolder()
}
},
{
id: 'Comfy-Desktop.Folders.OpenModelConfig',
label: 'Open extra_model_paths.yaml',
icon: 'pi pi-file',
function() {
electronAPI.openModelConfig()
}
},
{
id: 'Comfy-Desktop.OpenDevTools',
label: 'Open DevTools',
icon: 'pi pi-code',
function() {
electronAPI.openDevTools()
}
},
{
id: 'Comfy-Desktop.OpenFeedbackPage',
label: 'Feedback',
icon: 'pi pi-envelope',
function() {
window.open('https://forum.comfy.org/c/v1-feedback/', '_blank')
}
},
{
id: 'Comfy-Desktop.Reinstall',
label: 'Reinstall',
icon: 'pi pi-refresh',
function() {
// TODO(huchenlei): Add a confirmation dialog.
electronAPI.reinstall()
}
}
],
menuCommands: [
{
path: ['Help'],
commands: ['Comfy-Desktop.OpenFeedbackPage']
},
{
path: ['Help'],
commands: ['Comfy-Desktop.OpenDevTools']
},
{
path: ['Help', 'Open Folder'],
commands: [
'Comfy-Desktop.Folders.OpenLogsFolder',
'Comfy-Desktop.Folders.OpenModelsFolder',
'Comfy-Desktop.Folders.OpenOutputsFolder',
'Comfy-Desktop.Folders.OpenInputsFolder',
'Comfy-Desktop.Folders.OpenCustomNodesFolder',
'Comfy-Desktop.Folders.OpenModelConfig'
]
},
{
path: ['Help'],
commands: ['Comfy-Desktop.Reinstall']
}
],
aboutPageBadges: [
{
label: 'ComfyUI_Desktop ' + desktopAppVersion,
url: 'https://github.com/Comfy-Org/electron',
icon: 'pi pi-github'
}
]
})
})()

View File

@@ -13,7 +13,6 @@ import {
deserialiseAndCreate, deserialiseAndCreate,
serialise serialise
} from '@/extensions/core/vintageClipboard' } from '@/extensions/core/vintageClipboard'
import type { ComfyNodeDef } from '@/types/apiTypes'
type GroupNodeWorkflowData = { type GroupNodeWorkflowData = {
external: ComfyLink[] external: ComfyLink[]
@@ -57,7 +56,7 @@ const Workflow = {
class GroupNodeBuilder { class GroupNodeBuilder {
nodes: LGraphNode[] nodes: LGraphNode[]
nodeData: GroupNodeWorkflowData nodeData: any
constructor(nodes: LGraphNode[]) { constructor(nodes: LGraphNode[]) {
this.nodes = nodes this.nodes = nodes
@@ -176,7 +175,7 @@ export class GroupNodeConfig {
primitiveToWidget: {} primitiveToWidget: {}
nodeInputs: {} nodeInputs: {}
outputVisibility: any[] outputVisibility: any[]
nodeDef: ComfyNodeDef nodeDef: any
inputs: any[] inputs: any[]
linksFrom: {} linksFrom: {}
linksTo: {} linksTo: {}
@@ -205,7 +204,6 @@ export class GroupNodeConfig {
output: [], output: [],
output_name: [], output_name: [],
output_is_list: [], output_is_list: [],
// @ts-expect-error Unused, doesn't exist
output_is_hidden: [], output_is_hidden: [],
name: source + SEPARATOR + this.name, name: source + SEPARATOR + this.name,
display_name: this.name, display_name: this.name,
@@ -697,11 +695,11 @@ export class GroupNodeConfig {
} }
export class GroupNodeHandler { export class GroupNodeHandler {
node: LGraphNode node
groupData groupData
innerNodes: any innerNodes: any
constructor(node: LGraphNode) { constructor(node) {
this.node = node this.node = node
this.groupData = node.constructor?.nodeData?.[GROUP] this.groupData = node.constructor?.nodeData?.[GROUP]
@@ -776,7 +774,6 @@ export class GroupNodeHandler {
this.node.updateLink = (link) => { this.node.updateLink = (link) => {
// Replace the group node reference with the internal node // Replace the group node reference with the internal node
// @ts-expect-error Can this be removed? Or replaced with: LLink.create(link.asSerialisable())
link = { ...link } link = { ...link }
const output = this.groupData.newToOldOutputMap[link.origin_slot] const output = this.groupData.newToOldOutputMap[link.origin_slot]
let innerNode = this.innerNodes[output.node.index] let innerNode = this.innerNodes[output.node.index]
@@ -968,20 +965,17 @@ export class GroupNodeHandler {
app.canvas.emitBeforeChange() app.canvas.emitBeforeChange()
try { const { newNodes, selectedIds } = addInnerNodes()
const { newNodes, selectedIds } = addInnerNodes() reconnectInputs(selectedIds)
reconnectInputs(selectedIds) reconnectOutputs(selectedIds)
reconnectOutputs(selectedIds) app.graph.remove(this.node)
app.graph.remove(this.node)
return newNodes app.canvas.emitAfterChange()
} finally {
app.canvas.emitAfterChange() return newNodes
}
} }
const getExtraMenuOptions = this.node.getExtraMenuOptions const getExtraMenuOptions = this.node.getExtraMenuOptions
// @ts-expect-error Should pass patched return value getExtraMenuOptions
this.node.getExtraMenuOptions = function (_, options) { this.node.getExtraMenuOptions = function (_, options) {
getExtraMenuOptions?.apply(this, arguments) getExtraMenuOptions?.apply(this, arguments)
@@ -994,7 +988,6 @@ export class GroupNodeHandler {
null, null,
{ {
content: 'Convert to nodes', content: 'Convert to nodes',
// @ts-expect-error
callback: () => { callback: () => {
return this.convertToNodes() return this.convertToNodes()
} }
@@ -1155,7 +1148,6 @@ export class GroupNodeHandler {
if ( if (
old.inputName !== 'image' && old.inputName !== 'image' &&
// @ts-expect-error Widget values
!widget.options.values.includes(widget.value) !widget.options.values.includes(widget.value)
) { ) {
widget.value = widget.options.values[0] widget.value = widget.options.values[0]
@@ -1362,7 +1354,6 @@ export class GroupNodeHandler {
if (!originNode) continue // this node is in the group if (!originNode) continue // this node is in the group
originNode.connect( originNode.connect(
originSlot, originSlot,
// @ts-expect-error Valid - uses deprecated interface. Required check: if (graph.getNodeById(this.node.id) !== this.node) report()
this.node.id, this.node.id,
this.groupData.oldToNewInputMap[targetId][targetSlot] this.groupData.oldToNewInputMap[targetId][targetSlot]
) )
@@ -1484,7 +1475,7 @@ function ungroupSelectedGroupNodes() {
const nodes = Object.values(app.canvas.selected_nodes ?? {}) const nodes = Object.values(app.canvas.selected_nodes ?? {})
for (const node of nodes) { for (const node of nodes) {
if (GroupNodeHandler.isGroupNode(node)) { if (GroupNodeHandler.isGroupNode(node)) {
node.convertToNodes?.() node['convertToNodes']?.()
} }
} }
} }

View File

@@ -4,7 +4,6 @@ import { app } from '../../scripts/app'
import { LGraphCanvas } from '@comfyorg/litegraph' import { LGraphCanvas } from '@comfyorg/litegraph'
import type { Positionable } from '@comfyorg/litegraph/dist/interfaces' import type { Positionable } from '@comfyorg/litegraph/dist/interfaces'
import type { LGraphNode } from '@comfyorg/litegraph' import type { LGraphNode } from '@comfyorg/litegraph'
import { useSettingStore } from '@/stores/settingStore'
function setNodeMode(node: LGraphNode, mode: number) { function setNodeMode(node: LGraphNode, mode: number) {
node.mode = mode node.mode = mode
@@ -12,8 +11,7 @@ function setNodeMode(node: LGraphNode, mode: number) {
} }
function addNodesToGroup(group: LGraphGroup, items: Iterable<Positionable>) { function addNodesToGroup(group: LGraphGroup, items: Iterable<Positionable>) {
const padding = useSettingStore().get('Comfy.GroupSelectedNodes.Padding') group.resizeTo([...group.children, ...items])
group.resizeTo([...group.children, ...items], padding)
} }
app.registerExtension({ app.registerExtension({
@@ -78,10 +76,7 @@ app.registerExtension({
content: 'Fit Group To Nodes', content: 'Fit Group To Nodes',
callback: () => { callback: () => {
group.recomputeInsideNodes() group.recomputeInsideNodes()
const padding = useSettingStore().get( group.resizeTo(group.children)
'Comfy.GroupSelectedNodes.Padding'
)
group.resizeTo(group.children, padding)
this.graph.change() this.graph.change()
} }
}) })

View File

@@ -15,9 +15,8 @@ import './rerouteNode'
import './saveImageExtraOutput' import './saveImageExtraOutput'
import './simpleTouchSupport' import './simpleTouchSupport'
import './slotDefaults' import './slotDefaults'
import './snapToGrid'
import './uploadImage' import './uploadImage'
import './webcamCapture' import './webcamCapture'
import './widgetInputs' import './widgetInputs'
import './uploadAudio' import './uploadAudio'
import './electronAdapter'
import './load3d'

View File

@@ -24,7 +24,7 @@ app.registerExtension({
} }
app.ui.settings.addSetting({ app.ui.settings.addSetting({
id, id,
category: ['LiteGraph', 'Menu', 'InvertMenuScrolling'], category: ['Comfy', 'Graph', 'InvertMenuScrolling'],
name: 'Invert Context Menu Scrolling', name: 'Invert Context Menu Scrolling',
type: 'boolean', type: 'boolean',
defaultValue: false, defaultValue: false,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -44,7 +44,7 @@ app.registerExtension({
lastTouch = getMultiTouchCenter(e) lastTouch = getMultiTouchCenter(e)
touchDist = getMultiTouchPos(e) touchDist = getMultiTouchPos(e)
app.canvas.pointer.isDown = false app.canvas.pointer_is_down = false
} }
} }
}, },
@@ -78,7 +78,7 @@ app.registerExtension({
touchTime = null touchTime = null
if (e.touches?.length === 2 && lastTouch && !e.ctrlKey && !e.shiftKey) { if (e.touches?.length === 2 && lastTouch && !e.ctrlKey && !e.shiftKey) {
e.preventDefault() // Prevent browser from zooming when two textareas are touched e.preventDefault() // Prevent browser from zooming when two textareas are touched
app.canvas.pointer.isDown = false app.canvas.pointer_is_down = false
touchZooming = true touchZooming = true
LiteGraph.closeAllContextMenus(window) LiteGraph.closeAllContextMenus(window)
@@ -137,7 +137,7 @@ LGraphCanvas.prototype.processMouseDown = function (e) {
if (touchZooming || touchCount) { if (touchZooming || touchCount) {
return return
} }
app.canvas.pointer.isDown = false // Prevent context menu from opening on second tap app.canvas.pointer_is_down = false // Prevent context menu from opening on second tap
return processMouseDown.apply(this, arguments) return processMouseDown.apply(this, arguments)
} }

View File

@@ -0,0 +1,202 @@
// @ts-strict-ignore
import type { SettingParams } from '@/types/settingTypes'
import { app } from '../../scripts/app'
import {
LGraphCanvas,
LGraphNode,
LGraphGroup,
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)
vec[1] =
LiteGraph.CANVAS_GRID_SIZE * Math.round(vec[1] / LiteGraph.CANVAS_GRID_SIZE)
return vec
}
app.registerExtension({
name: 'Comfy.SnapToGrid',
init() {
// Add setting to control grid size
app.ui.settings.addSetting({
id: 'Comfy.SnapToGrid.GridSize',
category: ['Comfy', 'Graph', 'GridSize'],
name: 'Snap to grid size',
type: 'slider',
attrs: {
min: 1,
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.',
defaultValue: LiteGraph.CANVAS_GRID_SIZE,
onChange(value) {
LiteGraph.CANVAS_GRID_SIZE = +value || 10
}
})
// Keep the 'pysssss.SnapToGrid' setting id so we don't need to migrate setting values.
// Using a new setting id can cause existing users to lose their existing settings.
const alwaysSnapToGrid = app.ui.settings.addSetting({
id: 'pysssss.SnapToGrid',
category: ['Comfy', 'Graph', 'AlwaysSnapToGrid'],
name: 'Always snap to grid',
type: 'boolean',
defaultValue: false,
versionAdded: '1.3.13'
} as SettingParams)
const shouldSnapToGrid = () => app.shiftDown || alwaysSnapToGrid.value
// After moving a node, if the shift key is down align it to grid
const onNodeMoved = app.canvas.onNodeMoved
app.canvas.onNodeMoved = function (node) {
const r = onNodeMoved?.apply(this, arguments)
if (shouldSnapToGrid()) {
// Ensure all selected nodes are realigned
for (const id in this.selected_nodes) {
this.selected_nodes[id].alignToGrid()
}
}
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
app.graph.onNodeAdded = function (node) {
const onResize = node.onResize
node.onResize = function () {
if (shouldSnapToGrid()) {
roundVectorToGrid(node.size)
}
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
const origDrawNode = LGraphCanvas.prototype.drawNode
LGraphCanvas.prototype.drawNode = function (node, ctx) {
if (
shouldSnapToGrid() &&
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]
let w, h
if (node.flags.collapsed) {
w = node._collapsed_width
h = LiteGraph.NODE_TITLE_HEIGHT
shiftY -= LiteGraph.NODE_TITLE_HEIGHT
} else {
w = node.size[0]
h = node.size[1]
const titleMode = node.constructor.title_mode
if (
titleMode !== LiteGraph.TRANSPARENT_TITLE &&
titleMode !== LiteGraph.NO_TITLE
) {
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
}
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
/**
* 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`.
*/
const groupMove = LGraphGroup.prototype.move
LGraphGroup.prototype.move = function (deltax, deltay, ignore_nodes) {
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 (
!selectedAndMovingGroup &&
app.canvas.selected_group === this &&
(deltax || deltay)
) {
selectedAndMovingGroup = this
}
// LiteGraph will call group.move both on mouse-move as well as mouse-up though we only want
// to snap on a mouse-up which we can determine by checking if `app.canvas.last_mouse_dragging`
// has been set to `false`. Essentially, this check here is the equivalent to calling an
// `LGraphGroup.prototype.onNodeMoved` if it had existed.
if (app.canvas.last_mouse_dragging === false && shouldSnapToGrid()) {
// After moving a group (while shouldSnapToGrid()), snap all the child nodes and, finally,
// align the group itself.
this.recomputeInsideNodes()
for (const node of this.nodes) {
node.alignToGrid()
}
LGraphNode.prototype.alignToGrid.apply(this)
}
return v
}
/**
* Handles drawing a group when, snapping the size when one is actively being resized tracking and/or
* drawing a ghost box when one is actively being moved. This mimics the node snapping behavior for
* both.
*/
const drawGroups = LGraphCanvas.prototype.drawGroups
LGraphCanvas.prototype.drawGroups = function (canvas, ctx) {
if (this.selected_group && shouldSnapToGrid()) {
if (this.selected_group_resizing) {
roundVectorToGrid(this.selected_group.size)
} else if (selectedAndMovingGroup) {
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)'
ctx.rect(x, y, ...(selectedAndMovingGroup.size as [number, number]))
ctx.fill()
ctx.stroke()
ctx.fillStyle = f
ctx.strokeStyle = s
}
} else if (!this.selected_group) {
selectedAndMovingGroup = null
}
return drawGroups.apply(this, arguments)
}
/** Handles adding a group in a snapping-enabled state. */
const onGroupAdd = LGraphCanvas.onGroupAdd
LGraphCanvas.onGroupAdd = function () {
const v = onGroupAdd.apply(app.canvas, arguments)
if (shouldSnapToGrid()) {
const lastGroup = app.graph.groups[app.graph.groups.length - 1]
if (lastGroup) {
roundVectorToGrid(lastGroup.pos)
roundVectorToGrid(lastGroup.size)
}
}
return v
}
}
})

View File

@@ -69,54 +69,51 @@ export function deserialiseAndCreate(data: string, canvas: LGraphCanvas): void {
const { graph, graph_mouse } = canvas const { graph, graph_mouse } = canvas
canvas.emitBeforeChange() canvas.emitBeforeChange()
try { graph.beforeChange()
graph.beforeChange()
const deserialised = JSON.parse(data) const deserialised = JSON.parse(data)
// Find the top left point of the boundary of all pasted nodes // Find the top left point of the boundary of all pasted nodes
const topLeft = [Infinity, Infinity] const topLeft = [Infinity, Infinity]
for (const { pos } of deserialised.nodes) { for (const { pos } of deserialised.nodes) {
if (topLeft[0] > pos[0]) topLeft[0] = pos[0] if (topLeft[0] > pos[0]) topLeft[0] = pos[0]
if (topLeft[1] > pos[1]) topLeft[1] = pos[1] if (topLeft[1] > pos[1]) topLeft[1] = pos[1]
}
// Silent default instead of throw
if (!Number.isFinite(topLeft[0]) || !Number.isFinite(topLeft[1])) {
topLeft[0] = graph_mouse[0]
topLeft[1] = graph_mouse[1]
}
// Create nodes
const nodes: LGraphNode[] = []
for (const info of deserialised.nodes) {
const node = LiteGraph.createNode(info.type)
if (!node) continue
node.configure(info)
// Paste to the bottom right of pointer
node.pos[0] += graph_mouse[0] - topLeft[0]
node.pos[1] += graph_mouse[1] - topLeft[1]
graph.add(node, true)
nodes.push(node)
}
// Create links
for (const info of deserialised.links) {
const relativeId = info[0]
const outNode = relativeId != null ? nodes[relativeId] : undefined
const inNode = nodes[info[2]]
if (outNode && inNode) outNode.connect(info[1], inNode, info[3])
else console.warn('Warning, nodes missing on pasting')
}
canvas.selectNodes(nodes)
graph.afterChange()
} finally {
canvas.emitAfterChange()
} }
// Silent default instead of throw
if (!Number.isFinite(topLeft[0]) || !Number.isFinite(topLeft[1])) {
topLeft[0] = graph_mouse[0]
topLeft[1] = graph_mouse[1]
}
// Create nodes
const nodes: LGraphNode[] = []
for (const info of deserialised.nodes) {
const node = LiteGraph.createNode(info.type)
if (!node) continue
node.configure(info)
// Paste to the bottom right of pointer
node.pos[0] += graph_mouse[0] - topLeft[0]
node.pos[1] += graph_mouse[1] - topLeft[1]
graph.add(node, true)
nodes.push(node)
}
// Create links
for (const info of deserialised.links) {
const relativeId = info[0]
const outNode = relativeId != null ? nodes[relativeId] : undefined
const inNode = nodes[info[2]]
if (outNode && inNode) outNode.connect(info[1], inNode, info[3])
else console.warn('Warning, nodes missing on pasting')
}
canvas.selectNodes(nodes)
graph.afterChange()
canvas.emitAfterChange()
} }

View File

@@ -894,7 +894,7 @@ app.registerExtension({
// Not a widget input or already handled input // Not a widget input or already handled input
if ( if (
!(input.type in ComfyWidgets) && !(input.type in ComfyWidgets) &&
!(input.widget?.[GET_CONFIG]?.()?.[0] instanceof Array) !(input.widget[GET_CONFIG]?.()?.[0] instanceof Array)
) { ) {
return r //also Not a ComfyWidgets input or combo (do nothing) return r //also Not a ComfyWidgets input or combo (do nothing)
} }

View File

@@ -0,0 +1,14 @@
import { useI18n } from 'vue-i18n'
import { markRaw } from 'vue'
import IntegratedTerminal from '@/components/bottomPanel/tabs/IntegratedTerminal.vue'
import { BottomPanelExtension } from '@/types/extensionTypes'
export const useIntegratedTerminalTab = (): BottomPanelExtension => {
const { t } = useI18n()
return {
id: 'integrated-terminal',
title: t('terminal'),
component: markRaw(IntegratedTerminal),
type: 'vue'
}
}

View File

@@ -1,25 +0,0 @@
import { useI18n } from 'vue-i18n'
import { markRaw } from 'vue'
import { BottomPanelExtension } from '@/types/extensionTypes'
import LogsTerminal from '@/components/bottomPanel/tabs/terminal/LogsTerminal.vue'
import CommandTerminal from '@/components/bottomPanel/tabs/terminal/CommandTerminal.vue'
export const useLogsTerminalTab = (): BottomPanelExtension => {
const { t } = useI18n()
return {
id: 'logs-terminal',
title: t('logs'),
component: markRaw(LogsTerminal),
type: 'vue'
}
}
export const useCommandTerminalTab = (): BottomPanelExtension => {
const { t } = useI18n()
return {
id: 'command-terminal',
title: t('terminal'),
component: markRaw(CommandTerminal),
type: 'vue'
}
}

View File

@@ -1,69 +0,0 @@
import { FitAddon } from '@xterm/addon-fit'
import { Terminal } from '@xterm/xterm'
import { debounce } from 'lodash'
import { onMounted, onUnmounted, Ref } from 'vue'
import '@xterm/xterm/css/xterm.css'
export function useTerminal(element: Ref<HTMLElement>) {
const fitAddon = new FitAddon()
const terminal = new Terminal({
convertEol: true
})
terminal.loadAddon(fitAddon)
onMounted(async () => {
terminal.open(element.value)
})
onUnmounted(() => {
terminal.dispose()
})
return {
terminal,
useAutoSize(
root: Ref<HTMLElement>,
autoRows: boolean = true,
autoCols: boolean = true,
onResize?: () => void
) {
const ensureValidRows = (rows: number | undefined) => {
if (rows == null || isNaN(rows)) {
return root.value?.clientHeight / 20
}
return rows
}
const ensureValidCols = (cols: number | undefined): number => {
if (cols == null || isNaN(cols)) {
// Sometimes this is NaN if so, estimate.
return root.value?.clientWidth / 8
}
return cols
}
const resize = () => {
const dims = fitAddon.proposeDimensions()
// Sometimes propose returns NaN, so we may need to estimate.
terminal.resize(
autoCols ? ensureValidCols(dims?.cols) : terminal.cols,
autoRows ? ensureValidRows(dims?.rows) : terminal.rows
)
onResize?.()
}
const resizeObserver = new ResizeObserver(debounce(resize, 25))
onMounted(async () => {
resizeObserver.observe(root.value)
resize()
})
onUnmounted(() => {
resizeObserver.disconnect()
})
return { resize }
}
}
}

View File

@@ -2,28 +2,15 @@ import { markRaw } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import ModelLibrarySidebarTab from '@/components/sidebar/tabs/ModelLibrarySidebarTab.vue' import ModelLibrarySidebarTab from '@/components/sidebar/tabs/ModelLibrarySidebarTab.vue'
import type { SidebarTabExtension } from '@/types/extensionTypes' import type { SidebarTabExtension } from '@/types/extensionTypes'
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
import { isElectron } from '@/utils/envUtil'
export const useModelLibrarySidebarTab = (): SidebarTabExtension => { export const useModelLibrarySidebarTab = (): SidebarTabExtension => {
const { t } = useI18n() const { t } = useI18n()
return { return {
id: 'model-library', id: 'model-library',
icon: 'pi pi-box', icon: 'pi pi-box',
title: t('sideToolbar.modelLibrary'), title: t('sideToolbar.modelLibrary'),
tooltip: t('sideToolbar.modelLibrary'), tooltip: t('sideToolbar.modelLibrary'),
component: markRaw(ModelLibrarySidebarTab), component: markRaw(ModelLibrarySidebarTab),
type: 'vue', type: 'vue'
iconBadge: () => {
if (isElectron()) {
const electronDownloadStore = useElectronDownloadStore()
if (electronDownloadStore.downloads.length > 0) {
return electronDownloadStore.downloads.length.toString()
}
}
return null
}
} }
} }

View File

@@ -47,9 +47,7 @@ const messages = {
systemInfo: 'Operating system and app version', systemInfo: 'Operating system and app version',
personalInformation: 'Personal information', personalInformation: 'Personal information',
workflowContent: 'Workflow content', workflowContent: 'Workflow content',
fileSystemInformation: 'File system information', fileSystemInformation: 'File system information'
workflowContents: 'Workflow contents',
customNodeConfigurations: 'Custom node configurations'
} }
} }
}, },
@@ -59,7 +57,6 @@ const messages = {
loadAllFolders: 'Load All Folders', loadAllFolders: 'Load All Folders',
refresh: 'Refresh', refresh: 'Refresh',
terminal: 'Terminal', terminal: 'Terminal',
logs: 'Logs',
videoFailedToLoad: 'Video failed to load', videoFailedToLoad: 'Video failed to load',
extensionName: 'Extension Name', extensionName: 'Extension Name',
reloadToApplyChanges: 'Reload to apply changes', reloadToApplyChanges: 'Reload to apply changes',
@@ -128,12 +125,7 @@ const messages = {
backToAllTasks: 'Back to All Tasks', backToAllTasks: 'Back to All Tasks',
containImagePreview: 'Fill Image Preview', containImagePreview: 'Fill Image Preview',
coverImagePreview: 'Fit Image Preview', coverImagePreview: 'Fit Image Preview',
clearPendingTasks: 'Clear Pending Tasks', clearPendingTasks: 'Clear Pending Tasks'
filter: 'Filter Outputs',
filters: {
hideCached: 'Hide Cached',
hideCanceled: 'Hide Canceled'
}
} }
}, },
menu: { menu: {

View File

@@ -8,9 +8,7 @@ import {
import LayoutDefault from '@/views/layouts/LayoutDefault.vue' import LayoutDefault from '@/views/layouts/LayoutDefault.vue'
import { isElectron } from './utils/envUtil' import { isElectron } from './utils/envUtil'
const isFileProtocol = window.location.protocol === 'file:' const isFileProtocol = () => window.location.protocol === 'file:'
const basePath = isElectron() ? '/' : window.location.pathname
const guardElectronAccess = ( const guardElectronAccess = (
to: RouteLocationNormalized, to: RouteLocationNormalized,
from: RouteLocationNormalized, from: RouteLocationNormalized,
@@ -24,12 +22,12 @@ const guardElectronAccess = (
} }
const router = createRouter({ const router = createRouter({
history: isFileProtocol history: isFileProtocol()
? createWebHashHistory() ? createWebHashHistory()
: // Base path must be specified to ensure correct relative paths : // Base path must be specified to ensure correct relative paths
// Example: For URL 'http://localhost:7801/ComfyBackendDirect', // Example: For URL 'http://localhost:7801/ComfyBackendDirect',
// we need this base path or assets will incorrectly resolve from 'http://localhost:7801/' // we need this base path or assets will incorrectly resolve from 'http://localhost:7801/'
createWebHistory(basePath), createWebHistory(window.location.pathname),
routes: [ routes: [
{ {
path: '/', path: '/',

View File

@@ -1089,6 +1089,7 @@ export class ComfyApp {
'dragover', 'dragover',
(e) => { (e) => {
this.canvas.adjustMouseEvent(e) this.canvas.adjustMouseEvent(e)
// @ts-expect-error: canvasX and canvasY are added by adjustMouseEvent in litegraph
const node = this.graph.getNodeOnPos(e.canvasX, e.canvasY) const node = this.graph.getNodeOnPos(e.canvasX, e.canvasY)
if (node) { if (node) {
// @ts-expect-error This is not a standard event. TODO fix it. // @ts-expect-error This is not a standard event. TODO fix it.
@@ -1226,8 +1227,7 @@ export class ComfyApp {
const origProcessMouseDown = LGraphCanvas.prototype.processMouseDown const origProcessMouseDown = LGraphCanvas.prototype.processMouseDown
LGraphCanvas.prototype.processMouseDown = function (e) { LGraphCanvas.prototype.processMouseDown = function (e) {
// prepare for ctrl+shift drag: zoom start // prepare for ctrl+shift drag: zoom start
const useFastZoom = useSettingStore().get('Comfy.Graph.CtrlShiftZoom') if (e.ctrlKey && e.shiftKey && e.buttons) {
if (useFastZoom && e.ctrlKey && e.shiftKey && !e.altKey && e.buttons) {
self.zoom_drag_start = [e.x, e.y, this.ds.scale] self.zoom_drag_start = [e.x, e.y, this.ds.scale]
return return
} }
@@ -1572,7 +1572,10 @@ export class ComfyApp {
api.addEventListener('execution_start', ({ detail }) => { api.addEventListener('execution_start', ({ detail }) => {
this.lastExecutionError = null this.lastExecutionError = null
this.graph.nodes.forEach((node) => { this.graph.nodes.forEach((node) => {
if (node.onExecutionStart) node.onExecutionStart() // @ts-expect-error
if (node.onExecutionStart)
// @ts-expect-error
node.onExecutionStart()
}) })
}) })
@@ -1864,6 +1867,15 @@ export class ComfyApp {
await this.loadGraphData() await this.loadGraphData()
} }
// Save current workflow automatically
setInterval(() => {
const workflow = JSON.stringify(this.serializeGraph())
localStorage.setItem('workflow', workflow)
if (api.clientId) {
sessionStorage.setItem(`workflow:${api.clientId}`, workflow)
}
}, 1000)
this.#addDrawNodeHandler() this.#addDrawNodeHandler()
this.#addDrawGroupsHandler() this.#addDrawGroupsHandler()
this.#addDropHandler() this.#addDropHandler()
@@ -2395,8 +2407,8 @@ export class ComfyApp {
} }
} }
const innerNodes = outerNode.getInnerNodes const innerNodes = outerNode['getInnerNodes']
? outerNode.getInnerNodes() ? outerNode['getInnerNodes']()
: [outerNode] : [outerNode]
for (const node of innerNodes) { for (const node of innerNodes) {
if (node.isVirtualNode) { if (node.isVirtualNode) {
@@ -2414,8 +2426,8 @@ export class ComfyApp {
for (const outerNode of graph.computeExecutionOrder(false)) { for (const outerNode of graph.computeExecutionOrder(false)) {
const skipNode = outerNode.mode === 2 || outerNode.mode === 4 const skipNode = outerNode.mode === 2 || outerNode.mode === 4
const innerNodes = const innerNodes =
!skipNode && outerNode.getInnerNodes !skipNode && outerNode['getInnerNodes']
? outerNode.getInnerNodes() ? outerNode['getInnerNodes']()
: [outerNode] : [outerNode]
for (const node of innerNodes) { for (const node of innerNodes) {
if (node.isVirtualNode) { if (node.isVirtualNode) {
@@ -2881,6 +2893,7 @@ export class ComfyApp {
for (let nodeNum in this.graph.nodes) { for (let nodeNum in this.graph.nodes) {
const node = this.graph.nodes[nodeNum] const node = this.graph.nodes[nodeNum]
const def = defs[node.type] const def = defs[node.type]
// @ts-expect-error
// Allow primitive nodes to handle refresh // Allow primitive nodes to handle refresh
node.refreshComboInNode?.(defs) node.refreshComboInNode?.(defs)

View File

@@ -99,7 +99,7 @@ export class ChangeTracker {
this.initialState, this.initialState,
this.activeState this.activeState
) )
if (logger.getLevel() <= logger.levels.DEBUG && workflow.isModified) { if (workflow.isModified) {
const diff = ChangeTracker.graphDiff( const diff = ChangeTracker.graphDiff(
this.initialState, this.initialState,
this.activeState this.activeState

View File

@@ -63,36 +63,17 @@ export const workflowService = {
const newPath = workflow.directory + '/' + appendJsonExt(newFilename) const newPath = workflow.directory + '/' + appendJsonExt(newFilename)
const newKey = newPath.substring(ComfyWorkflow.basePath.length) const newKey = newPath.substring(ComfyWorkflow.basePath.length)
const workflowStore = useWorkflowStore()
const existingWorkflow = workflowStore.getWorkflowByPath(newPath)
if (existingWorkflow) {
const res = (await ComfyAsyncDialog.prompt({
title: 'Overwrite existing file?',
message: `"${newPath}" already exists. Do you want to overwrite it?`,
actions: ['Yes', 'No']
})) as 'Yes' | 'No'
if (res === 'No') return
if (existingWorkflow.path === workflow.path) {
await this.saveWorkflow(workflow)
return
}
const deleted = await this.deleteWorkflow(existingWorkflow)
if (!deleted) return
}
if (workflow.isTemporary) { if (workflow.isTemporary) {
await this.renameWorkflow(workflow, newPath) await this.renameWorkflow(workflow, newPath)
await workflowStore.saveWorkflow(workflow) await useWorkflowStore().saveWorkflow(workflow)
} else { } else {
const tempWorkflow = workflowStore.createTemporary( const tempWorkflow = useWorkflowStore().createTemporary(
newKey, newKey,
workflow.activeState as ComfyWorkflowJSON workflow.activeState as ComfyWorkflowJSON
) )
await this.openWorkflow(tempWorkflow) await this.openWorkflow(tempWorkflow)
await workflowStore.saveWorkflow(tempWorkflow) await useWorkflowStore().saveWorkflow(tempWorkflow)
} }
}, },
@@ -150,9 +131,9 @@ export const workflowService = {
async closeWorkflow( async closeWorkflow(
workflow: ComfyWorkflow, workflow: ComfyWorkflow,
options: { warnIfUnsaved: boolean } = { warnIfUnsaved: true } options: { warnIfUnsaved: boolean } = { warnIfUnsaved: true }
): Promise<boolean> { ): Promise<void> {
if (!workflow.isLoaded) { if (!workflow.isLoaded) {
return true return
} }
if (workflow.isModified && options.warnIfUnsaved) { if (workflow.isModified && options.warnIfUnsaved) {
@@ -165,7 +146,7 @@ export const workflowService = {
if (res === 'Yes') { if (res === 'Yes') {
await this.saveWorkflow(workflow) await this.saveWorkflow(workflow)
} else if (res === 'Cancel') { } else if (res === 'Cancel') {
return false return
} }
} }
@@ -180,26 +161,18 @@ export const workflowService = {
} }
await workflowStore.closeWorkflow(workflow) await workflowStore.closeWorkflow(workflow)
return true
}, },
async renameWorkflow(workflow: ComfyWorkflow, newPath: string) { async renameWorkflow(workflow: ComfyWorkflow, newPath: string) {
await useWorkflowStore().renameWorkflow(workflow, newPath) await useWorkflowStore().renameWorkflow(workflow, newPath)
}, },
/** async deleteWorkflow(workflow: ComfyWorkflow) {
* Delete a workflow
* @param workflow The workflow to delete
* @returns true if the workflow was deleted, false if the user cancelled
*/
async deleteWorkflow(workflow: ComfyWorkflow): Promise<boolean> {
const workflowStore = useWorkflowStore() const workflowStore = useWorkflowStore()
if (workflowStore.isOpen(workflow)) { if (workflowStore.isOpen(workflow)) {
const closed = await this.closeWorkflow(workflow) await this.closeWorkflow(workflow)
if (!closed) return false
} }
await workflowStore.deleteWorkflow(workflow) await workflowStore.deleteWorkflow(workflow)
return true
}, },
/** /**

View File

@@ -59,7 +59,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
key: 's', key: 's',
ctrl: true ctrl: true
}, },
commandId: 'Comfy.SaveWorkflow' commandId: 'Comfy.ExportWorkflow'
}, },
{ {
combo: { combo: {
@@ -173,7 +173,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
key: '`', key: '`',
ctrl: true ctrl: true
}, },
commandId: 'Workspace.ToggleBottomPanelTab.logs-terminal' commandId: 'Workspace.ToggleBottomPanelTab.integrated-terminal'
}, },
{ {
combo: { combo: {

View File

@@ -1,6 +1,9 @@
import type { Keybinding } from '@/types/keyBindingTypes' import type { Keybinding } from '@/types/keyBindingTypes'
import { NodeBadgeMode } from '@/types/nodeSource' import { NodeBadgeMode } from '@/types/nodeSource'
import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes' import {
LinkReleaseTriggerAction,
LinkReleaseTriggerMode
} from '@/types/searchBoxTypes'
import type { SettingParams } from '@/types/settingTypes' import type { SettingParams } from '@/types/settingTypes'
import { LinkMarkerShape } from '@comfyorg/litegraph' import { LinkMarkerShape } from '@comfyorg/litegraph'
import { LiteGraph } from '@comfyorg/litegraph' import { LiteGraph } from '@comfyorg/litegraph'
@@ -21,9 +24,17 @@ export const CORE_SETTINGS: SettingParams[] = [
options: ['default', 'litegraph (legacy)'], options: ['default', 'litegraph (legacy)'],
defaultValue: 'default' defaultValue: 'default'
}, },
{
id: 'Comfy.NodeSearchBoxImpl.LinkReleaseTrigger',
category: ['Comfy', 'Node Search Box', 'LinkReleaseTrigger'],
name: 'Trigger on link release',
type: 'hidden',
options: Object.values(LinkReleaseTriggerMode),
defaultValue: LinkReleaseTriggerMode.ALWAYS,
deprecated: true
},
{ {
id: 'Comfy.LinkRelease.Action', id: 'Comfy.LinkRelease.Action',
category: ['LiteGraph', 'LinkRelease', 'Action'],
name: 'Action on link release (No modifier)', name: 'Action on link release (No modifier)',
type: 'combo', type: 'combo',
options: Object.values(LinkReleaseTriggerAction), options: Object.values(LinkReleaseTriggerAction),
@@ -31,7 +42,6 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.LinkRelease.ActionShift', id: 'Comfy.LinkRelease.ActionShift',
category: ['LiteGraph', 'LinkRelease', 'ActionShift'],
name: 'Action on link release (Shift)', name: 'Action on link release (Shift)',
type: 'combo', type: 'combo',
options: Object.values(LinkReleaseTriggerAction), options: Object.values(LinkReleaseTriggerAction),
@@ -71,7 +81,7 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.Sidebar.Location', id: 'Comfy.Sidebar.Location',
category: ['Appearance', 'Sidebar', 'Location'], category: ['Comfy', 'Sidebar', 'Location'],
name: 'Sidebar location', name: 'Sidebar location',
type: 'combo', type: 'combo',
options: ['left', 'right'], options: ['left', 'right'],
@@ -79,7 +89,7 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.Sidebar.Size', id: 'Comfy.Sidebar.Size',
category: ['Appearance', 'Sidebar', 'Size'], category: ['Comfy', 'Sidebar', 'Size'],
name: 'Sidebar size', name: 'Sidebar size',
type: 'combo', type: 'combo',
options: ['normal', 'small'], options: ['normal', 'small'],
@@ -87,7 +97,7 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.TextareaWidget.FontSize', id: 'Comfy.TextareaWidget.FontSize',
category: ['Appearance', 'Node Widget', 'TextareaWidget', 'FontSize'], category: ['Comfy', 'Node Widget', 'TextareaWidget', 'FontSize'],
name: 'Textarea widget font size', name: 'Textarea widget font size',
type: 'slider', type: 'slider',
defaultValue: 10, defaultValue: 10,
@@ -111,8 +121,7 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.Graph.CanvasInfo', id: 'Comfy.Graph.CanvasInfo',
category: ['LiteGraph', 'Canvas', 'CanvasInfo'], name: 'Show canvas info (fps, etc.)',
name: 'Show canvas info on bottom left corner (fps, etc.)',
type: 'boolean', type: 'boolean',
defaultValue: true defaultValue: true
}, },
@@ -134,7 +143,6 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.Node.Opacity', id: 'Comfy.Node.Opacity',
category: ['Appearance', 'Node', 'Opacity'],
name: 'Node opacity', name: 'Node opacity',
type: 'slider', type: 'slider',
defaultValue: 1, defaultValue: 1,
@@ -159,7 +167,6 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.Graph.ZoomSpeed', id: 'Comfy.Graph.ZoomSpeed',
category: ['LiteGraph', 'Canvas', 'ZoomSpeed'],
name: 'Canvas zoom speed', name: 'Canvas zoom speed',
type: 'slider', type: 'slider',
defaultValue: 1.1, defaultValue: 1.1,
@@ -198,28 +205,8 @@ export const CORE_SETTINGS: SettingParams[] = [
type: 'hidden', type: 'hidden',
defaultValue: 'cover' defaultValue: 'cover'
}, },
// Hidden setting used by the queue for if the results should be grouped by prompt
{
id: 'Comfy.Queue.ShowFlatList',
name: 'Queue show flat list',
type: 'hidden',
defaultValue: false
},
// Hidden setting used by the queue to filter certain results
{
id: 'Comfy.Queue.Filter',
name: 'Queue output filters',
type: 'hidden',
defaultValue: {
hideCanceled: false,
hideCached: false
},
versionAdded: '1.4.3'
},
{ {
id: 'Comfy.GroupSelectedNodes.Padding', id: 'Comfy.GroupSelectedNodes.Padding',
category: ['LiteGraph', 'Group', 'Padding'],
name: 'Group selected nodes padding', name: 'Group selected nodes padding',
type: 'slider', type: 'slider',
defaultValue: 10, defaultValue: 10,
@@ -230,14 +217,12 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.Node.DoubleClickTitleToEdit', id: 'Comfy.Node.DoubleClickTitleToEdit',
category: ['LiteGraph', 'Node', 'DoubleClickTitleToEdit'],
name: 'Double click node title to edit', name: 'Double click node title to edit',
type: 'boolean', type: 'boolean',
defaultValue: true defaultValue: true
}, },
{ {
id: 'Comfy.Group.DoubleClickTitleToEdit', id: 'Comfy.Group.DoubleClickTitleToEdit',
category: ['LiteGraph', 'Group', 'DoubleClickTitleToEdit'],
name: 'Double click group title to edit', name: 'Double click group title to edit',
type: 'boolean', type: 'boolean',
defaultValue: true defaultValue: true
@@ -285,7 +270,6 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.NodeBadge.NodeSourceBadgeMode', id: 'Comfy.NodeBadge.NodeSourceBadgeMode',
category: ['LiteGraph', 'Node', 'NodeSourceBadgeMode'],
name: 'Node source badge mode', name: 'Node source badge mode',
type: 'combo', type: 'combo',
options: Object.values(NodeBadgeMode), options: Object.values(NodeBadgeMode),
@@ -293,7 +277,6 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.NodeBadge.NodeIdBadgeMode', id: 'Comfy.NodeBadge.NodeIdBadgeMode',
category: ['LiteGraph', 'Node', 'NodeIdBadgeMode'],
name: 'Node ID badge mode', name: 'Node ID badge mode',
type: 'combo', type: 'combo',
options: [NodeBadgeMode.None, NodeBadgeMode.ShowAll], options: [NodeBadgeMode.None, NodeBadgeMode.ShowAll],
@@ -301,7 +284,6 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.NodeBadge.NodeLifeCycleBadgeMode', id: 'Comfy.NodeBadge.NodeLifeCycleBadgeMode',
category: ['LiteGraph', 'Node', 'NodeLifeCycleBadgeMode'],
name: 'Node life cycle badge mode', name: 'Node life cycle badge mode',
type: 'combo', type: 'combo',
options: [NodeBadgeMode.None, NodeBadgeMode.ShowAll], options: [NodeBadgeMode.None, NodeBadgeMode.ShowAll],
@@ -334,7 +316,7 @@ export const CORE_SETTINGS: SettingParams[] = [
*/ */
{ {
id: 'Comfy.PreviewFormat', id: 'Comfy.PreviewFormat',
category: ['LiteGraph', 'Node Widget', 'PreviewFormat'], category: ['Comfy', 'Node Widget', 'PreviewFormat'],
name: 'Preview image format', name: 'Preview image format',
tooltip: tooltip:
'When displaying a preview in the image widget, convert it to a lightweight image, e.g. webp, jpeg, webp;50, etc.', 'When displaying a preview in the image widget, convert it to a lightweight image, e.g. webp, jpeg, webp;50, etc.',
@@ -343,14 +325,14 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.DisableSliders', id: 'Comfy.DisableSliders',
category: ['LiteGraph', 'Node Widget', 'DisableSliders'], category: ['Comfy', 'Node Widget', 'DisableSliders'],
name: 'Disable node widget sliders', name: 'Disable node widget sliders',
type: 'boolean', type: 'boolean',
defaultValue: false defaultValue: false
}, },
{ {
id: 'Comfy.DisableFloatRounding', id: 'Comfy.DisableFloatRounding',
category: ['LiteGraph', 'Node Widget', 'DisableFloatRounding'], category: ['Comfy', 'Node Widget', 'DisableFloatRounding'],
name: 'Disable default float widget rounding.', name: 'Disable default float widget rounding.',
tooltip: tooltip:
'(requires page reload) Cannot disable round when round is set by the node in the backend.', '(requires page reload) Cannot disable round when round is set by the node in the backend.',
@@ -359,7 +341,7 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.FloatRoundingPrecision', id: 'Comfy.FloatRoundingPrecision',
category: ['LiteGraph', 'Node Widget', 'FloatRoundingPrecision'], category: ['Comfy', 'Node Widget', 'FloatRoundingPrecision'],
name: 'Float widget rounding decimal places [0 = auto].', name: 'Float widget rounding decimal places [0 = auto].',
tooltip: '(requires page reload)', tooltip: '(requires page reload)',
type: 'slider', type: 'slider',
@@ -372,7 +354,7 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.EnableTooltips', id: 'Comfy.EnableTooltips',
category: ['LiteGraph', 'Node', 'EnableTooltips'], category: ['Comfy', 'Node', 'EnableTooltips'],
name: 'Enable Tooltips', name: 'Enable Tooltips',
type: 'boolean', type: 'boolean',
defaultValue: true defaultValue: true
@@ -413,7 +395,6 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.Graph.CanvasMenu', id: 'Comfy.Graph.CanvasMenu',
category: ['LiteGraph', 'Canvas', 'CanvasMenu'],
name: 'Show graph canvas menu', name: 'Show graph canvas menu',
type: 'boolean', type: 'boolean',
defaultValue: true defaultValue: true
@@ -467,7 +448,7 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.LinkRenderMode', id: 'Comfy.LinkRenderMode',
category: ['LiteGraph', 'Graph', 'LinkRenderMode'], category: ['Comfy', 'Graph', 'LinkRenderMode'],
name: 'Link Render Mode', name: 'Link Render Mode',
defaultValue: 2, defaultValue: 2,
type: 'combo', type: 'combo',
@@ -480,7 +461,6 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.Node.AutoSnapLinkToSlot', id: 'Comfy.Node.AutoSnapLinkToSlot',
category: ['LiteGraph', 'Node', 'AutoSnapLinkToSlot'],
name: 'Auto snap link to node slot', name: 'Auto snap link to node slot',
tooltip: tooltip:
'When dragging a link over a node, the link automatically snap to a viable input slot on the node', 'When dragging a link over a node, the link automatically snap to a viable input slot on the node',
@@ -490,7 +470,6 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.Node.SnapHighlightsNode', id: 'Comfy.Node.SnapHighlightsNode',
category: ['LiteGraph', 'Node', 'SnapHighlightsNode'],
name: 'Snap highlights node', name: 'Snap highlights node',
tooltip: tooltip:
'When dragging a link over a node with viable input slot, highlight the node', 'When dragging a link over a node with viable input slot, highlight the node',
@@ -500,7 +479,6 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.Node.BypassAllLinksOnDelete', id: 'Comfy.Node.BypassAllLinksOnDelete',
category: ['LiteGraph', 'Node', 'BypassAllLinksOnDelete'],
name: 'Keep all links when deleting nodes', name: 'Keep all links when deleting nodes',
tooltip: tooltip:
'When deleting a node, attempt to reconnect all of its input and output links (bypassing the deleted node)', 'When deleting a node, attempt to reconnect all of its input and output links (bypassing the deleted node)',
@@ -510,7 +488,6 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.Node.MiddleClickRerouteNode', id: 'Comfy.Node.MiddleClickRerouteNode',
category: ['LiteGraph', 'Node', 'MiddleClickRerouteNode'],
name: 'Middle-click creates a new Reroute node', name: 'Middle-click creates a new Reroute node',
type: 'boolean', type: 'boolean',
defaultValue: true, defaultValue: true,
@@ -518,7 +495,6 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.RerouteBeta', id: 'Comfy.RerouteBeta',
category: ['LiteGraph', 'RerouteBeta'],
name: 'Opt-in to the reroute beta test', name: 'Opt-in to the reroute beta test',
tooltip: tooltip:
'Enables the new native reroutes.\n\nReroutes can be added by holding alt and dragging from a link line, or on the link menu.\n\nDisabling this option is non-destructive - reroutes are hidden.', 'Enables the new native reroutes.\n\nReroutes can be added by holding alt and dragging from a link line, or on the link menu.\n\nDisabling this option is non-destructive - reroutes are hidden.',
@@ -529,7 +505,6 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.Graph.LinkMarkers', id: 'Comfy.Graph.LinkMarkers',
category: ['LiteGraph', 'Link', 'LinkMarkers'],
name: 'Link midpoint markers', name: 'Link midpoint markers',
defaultValue: LinkMarkerShape.Circle, defaultValue: LinkMarkerShape.Circle,
type: 'combo', type: 'combo',
@@ -542,87 +517,9 @@ export const CORE_SETTINGS: SettingParams[] = [
}, },
{ {
id: 'Comfy.DOMClippingEnabled', id: 'Comfy.DOMClippingEnabled',
category: ['LiteGraph', 'Node', 'DOMClippingEnabled'], category: ['Comfy', 'Node', 'DOMClippingEnabled'],
name: 'Enable DOM element clipping (enabling may reduce performance)', name: 'Enable DOM element clipping (enabling may reduce performance)',
type: 'boolean', type: 'boolean',
defaultValue: true defaultValue: true
},
{
id: 'Comfy.Graph.CtrlShiftZoom',
category: ['LiteGraph', 'Canvas', 'CtrlShiftZoom'],
name: 'Enable fast-zoom shortcut (Ctrl + Shift + Drag)',
type: 'boolean',
defaultValue: true,
versionAdded: '1.4.0'
},
{
id: 'Comfy.Pointer.ClickDrift',
category: ['LiteGraph', 'Pointer', 'ClickDrift'],
name: 'Pointer click drift (maximum distance)',
tooltip:
'If the pointer moves more than this distance while holding a button down, it is considered dragging (rather than clicking).\n\nHelps prevent objects from being unintentionally nudged if the pointer is moved whilst clicking.',
experimental: true,
type: 'slider',
attrs: {
min: 0,
max: 20,
step: 1
},
defaultValue: 6,
versionAdded: '1.4.3'
},
{
id: 'Comfy.Pointer.ClickBufferTime',
category: ['LiteGraph', 'Pointer', 'ClickBufferTime'],
name: 'Pointer click drift delay',
tooltip:
'After pressing a pointer button down, this is the maximum time (in milliseconds) that pointer movement can be ignored for.\n\nHelps prevent objects from being unintentionally nudged if the pointer is moved whilst clicking.',
experimental: true,
type: 'slider',
attrs: {
min: 0,
max: 1000,
step: 25
},
defaultValue: 150,
versionAdded: '1.4.3'
},
{
id: 'Comfy.Pointer.DoubleClickTime',
category: ['LiteGraph', 'Pointer', 'DoubleClickTime'],
name: 'Double click interval (maximum)',
tooltip:
'The maximum time in milliseconds between the two clicks of a double-click. Increasing this value may assist if double-clicks are sometimes not registered.',
type: 'slider',
attrs: {
min: 100,
max: 1000,
step: 50
},
defaultValue: 300,
versionAdded: '1.4.3'
},
{
id: 'Comfy.SnapToGrid.GridSize',
category: ['LiteGraph', 'Canvas', 'GridSize'],
name: 'Snap to grid size',
type: 'slider',
attrs: {
min: 1,
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.',
defaultValue: LiteGraph.CANVAS_GRID_SIZE
},
// Keep the 'pysssss.SnapToGrid' setting id so we don't need to migrate setting values.
// Using a new setting id can cause existing users to lose their existing settings.
{
id: 'pysssss.SnapToGrid',
category: ['LiteGraph', 'Canvas', 'AlwaysSnapToGrid'],
name: 'Always snap to grid',
type: 'boolean',
defaultValue: false,
versionAdded: '1.3.13'
} }
] ]

View File

@@ -30,7 +30,6 @@ export class ResultItemImpl {
filename: string filename: string
subfolder: string subfolder: string
type: string type: string
cached: boolean
nodeId: NodeId nodeId: NodeId
// 'audio' | 'images' | ... // 'audio' | 'images' | ...
@@ -44,7 +43,6 @@ export class ResultItemImpl {
this.filename = obj.filename ?? '' this.filename = obj.filename ?? ''
this.subfolder = obj.subfolder ?? '' this.subfolder = obj.subfolder ?? ''
this.type = obj.type ?? '' this.type = obj.type ?? ''
this.cached = obj.cached
this.nodeId = obj.nodeId this.nodeId = obj.nodeId
this.mediaType = obj.mediaType this.mediaType = obj.mediaType
@@ -151,13 +149,6 @@ export class TaskItemImpl {
if (!this.outputs) { if (!this.outputs) {
return [] return []
} }
const cachedEntries = new Set(
this.status?.messages?.find((message) => {
return message[0] === 'execution_cached'
})?.[1].nodes
)
return Object.entries(this.outputs).flatMap(([nodeId, nodeOutputs]) => return Object.entries(this.outputs).flatMap(([nodeId, nodeOutputs]) =>
Object.entries(nodeOutputs).flatMap(([mediaType, items]) => Object.entries(nodeOutputs).flatMap(([mediaType, items]) =>
(items as ResultItem[]).map( (items as ResultItem[]).map(
@@ -165,8 +156,7 @@ export class TaskItemImpl {
new ResultItemImpl({ new ResultItemImpl({
...item, ...item,
nodeId, nodeId,
mediaType, mediaType
cached: cachedEntries.has(nodeId)
}) })
) )
) )

View File

@@ -2,12 +2,8 @@ import type { BottomPanelExtension } from '@/types/extensionTypes'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useCommandStore } from '@/stores/commandStore' import { useCommandStore } from '@/stores/commandStore'
import { import { useIntegratedTerminalTab } from '@/hooks/bottomPanelTabs/integratedTerminalTab'
useLogsTerminalTab,
useCommandTerminalTab
} from '@/hooks/bottomPanelTabs/terminalTabs'
import { ComfyExtension } from '@/types/comfy' import { ComfyExtension } from '@/types/comfy'
import { isElectron } from '@/utils/envUtil'
export const useBottomPanelStore = defineStore('bottomPanel', () => { export const useBottomPanelStore = defineStore('bottomPanel', () => {
const bottomPanelVisible = ref(false) const bottomPanelVisible = ref(false)
@@ -53,10 +49,7 @@ export const useBottomPanelStore = defineStore('bottomPanel', () => {
} }
const registerCoreBottomPanelTabs = () => { const registerCoreBottomPanelTabs = () => {
registerBottomPanelTab(useLogsTerminalTab()) registerBottomPanelTab(useIntegratedTerminalTab())
if (isElectron()) {
registerBottomPanelTab(useCommandTerminalTab())
}
} }
const registerExtensionBottomPanelTabs = (extension: ComfyExtension) => { const registerExtensionBottomPanelTabs = (extension: ComfyExtension) => {

View File

@@ -454,11 +454,6 @@ const zNodeBadgeMode = z.enum(
Object.values(NodeBadgeMode) as [string, ...string[]] Object.values(NodeBadgeMode) as [string, ...string[]]
) )
const zQueueFilter = z.object({
hideCached: z.boolean(),
hideCanceled: z.boolean()
})
const zSettings = z.record(z.any()).and( const zSettings = z.record(z.any()).and(
z z
.object({ .object({
@@ -489,6 +484,11 @@ const zSettings = z.record(z.any()).and(
zBookmarkCustomization zBookmarkCustomization
), ),
'Comfy.NodeInputConversionSubmenus': z.boolean(), 'Comfy.NodeInputConversionSubmenus': z.boolean(),
'Comfy.NodeSearchBoxImpl.LinkReleaseTrigger': z.enum([
'always',
'hold shift',
'NOT hold shift'
]),
'Comfy.LinkRelease.Action': zLinkReleaseTriggerAction, 'Comfy.LinkRelease.Action': zLinkReleaseTriggerAction,
'Comfy.LinkRelease.ActionShift': zLinkReleaseTriggerAction, 'Comfy.LinkRelease.ActionShift': zLinkReleaseTriggerAction,
'Comfy.NodeSearchBoxImpl.NodePreview': z.boolean(), 'Comfy.NodeSearchBoxImpl.NodePreview': z.boolean(),
@@ -511,8 +511,6 @@ const zSettings = z.record(z.any()).and(
'Comfy.Validation.Workflows': z.boolean(), 'Comfy.Validation.Workflows': z.boolean(),
'Comfy.Workflow.SortNodeIdOnSave': z.boolean(), 'Comfy.Workflow.SortNodeIdOnSave': z.boolean(),
'Comfy.Queue.ImageFit': z.enum(['contain', 'cover']), 'Comfy.Queue.ImageFit': z.enum(['contain', 'cover']),
'Comfy.Queue.ShowFlatList': z.boolean(),
'Comfy.Queue.Filter': zQueueFilter.passthrough(),
'Comfy.Workflow.WorkflowTabsPosition': z.enum(['Sidebar', 'Topbar']), 'Comfy.Workflow.WorkflowTabsPosition': z.enum(['Sidebar', 'Topbar']),
'Comfy.Node.DoubleClickTitleToEdit': z.boolean(), 'Comfy.Node.DoubleClickTitleToEdit': z.boolean(),
'Comfy.Window.UnloadConfirmation': z.boolean(), 'Comfy.Window.UnloadConfirmation': z.boolean(),

View File

@@ -1,7 +1,6 @@
import '@comfyorg/litegraph' import '@comfyorg/litegraph'
import type { ComfyNodeDef } from '@/types/apiTypes' import type { ComfyNodeDef } from '@/types/apiTypes'
import type { LLink } from '@comfyorg/litegraph' import type { LLink } from '@comfyorg/litegraph'
import type { NodeId } from './comfyWorkflow'
/** /**
* ComfyUI extensions of litegraph * ComfyUI extensions of litegraph
@@ -27,17 +26,8 @@ declare module '@comfyorg/litegraph' {
onExecuted?(output: any): void onExecuted?(output: any): void
onNodeCreated?(this: LGraphNode): void onNodeCreated?(this: LGraphNode): void
setInnerNodes?(nodes: LGraphNode[]): void setInnerNodes?(nodes: LGraphNode[]): void
// TODO: Requires several coercion changes to runtime code.
getInnerNodes?() // : LGraphNode[]
convertToNodes?(): LGraphNode[]
recreate?(): Promise<LGraphNode>
refreshComboInNode?(defs: Record<string, ComfyNodeDef>)
applyToGraph?(extraLinks?: LLink[]): void applyToGraph?(extraLinks?: LLink[]): void
updateLink?(link: LLink): LLink | null updateLink?(link: LLink): LLink | null
onExecutionStart?(): unknown
index?: number
runningInternalNodeId?: NodeId
comfyClass?: string comfyClass?: string

View File

@@ -9,9 +9,6 @@ module.exports = async function () {
const { nop } = require('../utils/nopProxy') const { nop } = require('../utils/nopProxy')
global.enableWebGLCanvas = nop global.enableWebGLCanvas = nop
global.window.HTMLElement.prototype.hasPointerCapture = jest.fn(() => true)
global.window.HTMLElement.prototype.setPointerCapture = jest.fn()
HTMLCanvasElement.prototype.getContext = nop HTMLCanvasElement.prototype.getContext = nop
localStorage['Comfy.Settings.Comfy.Logging.Enabled'] = 'false' localStorage['Comfy.Settings.Comfy.Logging.Enabled'] = 'false'

View File

@@ -11,8 +11,6 @@ dotenv.config()
const IS_DEV = process.env.NODE_ENV === 'development' const IS_DEV = process.env.NODE_ENV === 'development'
const SHOULD_MINIFY = process.env.ENABLE_MINIFY === 'true' const SHOULD_MINIFY = process.env.ENABLE_MINIFY === 'true'
// vite dev server will listen on all addresses, including LAN and public addresses
const VITE_REMOTE_DEV = process.env.VITE_REMOTE_DEV === 'true'
interface ShimResult { interface ShimResult {
code: string code: string
@@ -96,7 +94,7 @@ const DEV_SERVER_COMFYUI_URL = process.env.DEV_SERVER_COMFYUI_URL || 'http://127
export default defineConfig({ export default defineConfig({
base: '', base: '',
server: { server: {
host: VITE_REMOTE_DEV ? '0.0.0.0' : undefined, host: '0.0.0.0',
proxy: { proxy: {
'/internal': { '/internal': {
target: DEV_SERVER_COMFYUI_URL, target: DEV_SERVER_COMFYUI_URL,

View File

@@ -20,7 +20,7 @@ const mockElectronAPI: Plugin = {
Promise.resolve({ Promise.resolve({
appData: 'C:/Users/username/AppData/Roaming', appData: 'C:/Users/username/AppData/Roaming',
appPath: 'C:/Program Files/comfyui-electron/resources/app', appPath: 'C:/Program Files/comfyui-electron/resources/app',
defaultInstallPath: 'bad' defaultInstallPath: 'C:/Users/username/comfyui-electron'
}), }),
validateInstallPath: (path) => { validateInstallPath: (path) => {
if (path === 'bad') { if (path === 'bad') {
@@ -42,12 +42,7 @@ const mockElectronAPI: Plugin = {
} }
return { isValid: true } return { isValid: true }
}, },
showDirectoryPicker: () => Promise.resolve('C:/Users/username/comfyui-electron/1'), showDirectoryPicker: () => Promise.resolve('C:/Users/username/comfyui-electron/1')
DownloadManager: {
getAllDownloads: () => Promise.resolve([]),
onDownloadProgress: () => {}
},
getElectronVersion: () => Promise.resolve('1.0.0')
};` };`
} }
] ]