Compare commits
89 Commits
node-templ
...
fix-queue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02c13c403f | ||
|
|
8254e3c9cf | ||
|
|
1160231b62 | ||
|
|
a51e27bedf | ||
|
|
abed0656af | ||
|
|
5febda16c7 | ||
|
|
069dc67c30 | ||
|
|
7623810166 | ||
|
|
21fa88461f | ||
|
|
27b0493306 | ||
|
|
ad2c1a0d3e | ||
|
|
f51866d988 | ||
|
|
46627bb44b | ||
|
|
68cadbda9f | ||
|
|
0f2260065a | ||
|
|
4007cc13c2 | ||
|
|
3920210c5c | ||
|
|
4e22bffae2 | ||
|
|
462a131557 | ||
|
|
ec01a04786 | ||
|
|
4c48241e19 | ||
|
|
886c40a69a | ||
|
|
479d1b28c7 | ||
|
|
c41b57128a | ||
|
|
5d178a407d | ||
|
|
73b7606f6e | ||
|
|
94f5031f0d | ||
|
|
c857e7d98c | ||
|
|
d5b8a555d9 | ||
|
|
f34d50da3d | ||
|
|
4f3693e322 | ||
|
|
431ad7d27f | ||
|
|
0c97b09a5a | ||
|
|
bdb9f0d845 | ||
|
|
77b85acdd5 | ||
|
|
8906f5c26e | ||
|
|
81194cc7fe | ||
|
|
f4b972fab5 | ||
|
|
3aa1c03566 | ||
|
|
600b7f93e5 | ||
|
|
2a7df57404 | ||
|
|
6352cd86ee | ||
|
|
0058691579 | ||
|
|
1531bb6d9f | ||
|
|
40245aacf9 | ||
|
|
6e49685f58 | ||
|
|
946823ce6c | ||
|
|
c05f1465db | ||
|
|
88164bdac5 | ||
|
|
fc9e347055 | ||
|
|
6fbf1248f4 | ||
|
|
56848724cd | ||
|
|
26c3eeb942 | ||
|
|
a8f869337e | ||
|
|
7e245ba1cf | ||
|
|
2a93f873b4 | ||
|
|
f8e7058e19 | ||
|
|
8d4e740baa | ||
|
|
3273ee938b | ||
|
|
94f1bc3b38 | ||
|
|
d5ce140eb6 | ||
|
|
b5f0c4bf73 | ||
|
|
545a990365 | ||
|
|
71e4a42cfe | ||
|
|
16b0ebf75a | ||
|
|
eaeb17bdc7 | ||
|
|
239b464957 | ||
|
|
00b6d989ec | ||
|
|
c5f05b1855 | ||
|
|
6fefcaad7b | ||
|
|
22fdfd7f0b | ||
|
|
6842eb05de | ||
|
|
37e7994d55 | ||
|
|
399893bbb2 | ||
|
|
227db065f3 | ||
|
|
b4352bcd8d | ||
|
|
39bab9d9e2 | ||
|
|
c71644f02f | ||
|
|
6aad7ee8b6 | ||
|
|
2b96d831fc | ||
|
|
dde0291add | ||
|
|
8af016ffc1 | ||
|
|
82b4547d7d | ||
|
|
791a25637f | ||
|
|
b922aa5c7c | ||
|
|
cbaebbc9c2 | ||
|
|
86b2e1aa6c | ||
|
|
61c5f05126 | ||
|
|
dde9c3dad5 |
@@ -6,6 +6,11 @@ PLAYWRIGHT_TEST_URL=http://localhost:5173
|
||||
# Note: localhost:8188 does not work.
|
||||
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 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`
|
||||
|
||||
2
.github/workflows/test-ui.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
run: |
|
||||
npm run test:generate
|
||||
npm run test:generate:examples
|
||||
npm test -- --verbose
|
||||
npm run test:jest:fast -- --verbose
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
playwright-tests-chromium:
|
||||
|
||||
@@ -431,6 +431,8 @@ core extensions will be loaded.
|
||||
|
||||
#### 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:
|
||||
|
||||
```
|
||||
|
||||
90
browser_tests/assets/oversized_group.json
Normal file
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"last_node_id": 9,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 3,
|
||||
"type": "KSampler",
|
||||
"pos": [
|
||||
37,
|
||||
98
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
262
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
156680208700286,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Group",
|
||||
"bounding": [
|
||||
23,
|
||||
23,
|
||||
900,
|
||||
825
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -11,7 +11,7 @@ test.describe('Browser tab title', () => {
|
||||
const workflowName = await comfyPage.page.evaluate(async () => {
|
||||
return window['app'].extensionManager.workflow.activeWorkflow.filename
|
||||
})
|
||||
expect(await comfyPage.page.title()).toBe(`${workflowName} - ComfyUI`)
|
||||
expect(await comfyPage.page.title()).toBe(`*${workflowName} - ComfyUI`)
|
||||
})
|
||||
|
||||
// Failing on CI
|
||||
|
||||
@@ -56,34 +56,33 @@ test.describe('Change Tracker', () => {
|
||||
expect(await comfyPage.getToastErrorCount()).toBe(0)
|
||||
expect(await isModified()).toBe(false)
|
||||
|
||||
// TODO(huchenlei): Investigate why saving the workflow is causing the
|
||||
// undo queue to be triggered.
|
||||
expect(await getUndoQueueSize()).toBe(1)
|
||||
expect(await getUndoQueueSize()).toBe(0)
|
||||
expect(await getRedoQueueSize()).toBe(0)
|
||||
|
||||
const node = (await comfyPage.getFirstNodeRef())!
|
||||
await node.click('title')
|
||||
await node.click('collapse')
|
||||
await expect(node).toBeCollapsed()
|
||||
expect(await isModified()).toBe(true)
|
||||
expect(await getUndoQueueSize()).toBe(2)
|
||||
expect(await getUndoQueueSize()).toBe(1)
|
||||
expect(await getRedoQueueSize()).toBe(0)
|
||||
|
||||
await comfyPage.ctrlB()
|
||||
await expect(node).toBeBypassed()
|
||||
expect(await isModified()).toBe(true)
|
||||
expect(await getUndoQueueSize()).toBe(3)
|
||||
expect(await getUndoQueueSize()).toBe(2)
|
||||
expect(await getRedoQueueSize()).toBe(0)
|
||||
|
||||
await comfyPage.ctrlZ()
|
||||
await expect(node).not.toBeBypassed()
|
||||
expect(await isModified()).toBe(true)
|
||||
expect(await getUndoQueueSize()).toBe(2)
|
||||
expect(await getUndoQueueSize()).toBe(1)
|
||||
expect(await getRedoQueueSize()).toBe(1)
|
||||
|
||||
await comfyPage.ctrlZ()
|
||||
await expect(node).not.toBeCollapsed()
|
||||
expect(await isModified()).toBe(false)
|
||||
expect(await getUndoQueueSize()).toBe(1)
|
||||
expect(await getUndoQueueSize()).toBe(0)
|
||||
expect(await getRedoQueueSize()).toBe(2)
|
||||
})
|
||||
})
|
||||
@@ -98,6 +97,7 @@ test.describe('Change Tracker', () => {
|
||||
|
||||
// Make changes outside set
|
||||
// Bypass + collapse node
|
||||
await node.click('title')
|
||||
await node.click('collapse')
|
||||
await comfyPage.ctrlB()
|
||||
await expect(node).toBeCollapsed()
|
||||
@@ -111,6 +111,10 @@ test.describe('Change Tracker', () => {
|
||||
await expect(node).not.toBeBypassed()
|
||||
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
|
||||
await beforeChange(comfyPage)
|
||||
|
||||
@@ -152,6 +156,7 @@ test.describe('Change Tracker', () => {
|
||||
const multipleChanges = async () => {
|
||||
await beforeChange(comfyPage)
|
||||
// Call other actions that uses begin/endChange
|
||||
await node.click('title')
|
||||
await collapse()
|
||||
await bypassAndPin()
|
||||
await afterChange(comfyPage)
|
||||
|
||||
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 151 KiB |
@@ -24,14 +24,6 @@ test.describe('Copy Paste', () => {
|
||||
|
||||
test('Can copy and paste widget value', async ({ comfyPage }) => {
|
||||
// Copy width value (512) from empty latent node to KSampler's seed.
|
||||
// Empty latent node's width
|
||||
await comfyPage.canvas.click({
|
||||
position: {
|
||||
x: 718,
|
||||
y: 643
|
||||
}
|
||||
})
|
||||
await comfyPage.ctrlC(null)
|
||||
// KSampler's seed
|
||||
await comfyPage.canvas.click({
|
||||
position: {
|
||||
@@ -39,6 +31,14 @@ test.describe('Copy Paste', () => {
|
||||
y: 281
|
||||
}
|
||||
})
|
||||
await comfyPage.ctrlC(null)
|
||||
// Empty latent node's width
|
||||
await comfyPage.canvas.click({
|
||||
position: {
|
||||
x: 718,
|
||||
y: 643
|
||||
}
|
||||
})
|
||||
await comfyPage.ctrlV(null)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('copied-widget-value.png')
|
||||
|
||||
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 98 KiB |
@@ -204,13 +204,15 @@ export class ComfyPage {
|
||||
}
|
||||
}
|
||||
|
||||
async setup() {
|
||||
async setup({ clearStorage = true }: { clearStorage?: boolean } = {}) {
|
||||
await this.goto()
|
||||
await this.page.evaluate((id) => {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
localStorage.setItem('Comfy.userId', id)
|
||||
}, this.id)
|
||||
if (clearStorage) {
|
||||
await this.page.evaluate((id) => {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
localStorage.setItem('Comfy.userId', id)
|
||||
}, this.id)
|
||||
}
|
||||
await this.goto()
|
||||
|
||||
// Unify font for consistent screenshots.
|
||||
@@ -314,9 +316,9 @@ export class ComfyPage {
|
||||
}, settingId)
|
||||
}
|
||||
|
||||
async reload() {
|
||||
async reload({ clearStorage = true }: { clearStorage?: boolean } = {}) {
|
||||
await this.page.reload({ timeout: 15000 })
|
||||
await this.setup()
|
||||
await this.setup({ clearStorage })
|
||||
}
|
||||
|
||||
async goto() {
|
||||
@@ -524,9 +526,6 @@ export class ComfyPage {
|
||||
safeSpot = safeSpot || { x: 10, y: 10 }
|
||||
await this.page.mouse.move(safeSpot.x, safeSpot.y)
|
||||
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.up()
|
||||
await this.nextFrame()
|
||||
@@ -555,6 +554,11 @@ export class ComfyPage {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async clickContextMenuItem(name: string): Promise<void> {
|
||||
await this.page.getByRole('menuitem', { name }).click()
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async doubleClickCanvas() {
|
||||
await this.page.mouse.dblclick(10, 10, { delay: 5 })
|
||||
await this.nextFrame()
|
||||
@@ -733,6 +737,19 @@ 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) {
|
||||
this.page.on('dialog', async (dialog) => {
|
||||
await dialog.accept(groupNodeName)
|
||||
|
||||
@@ -43,7 +43,7 @@ export class ComfyNodeSearchBox {
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@@ -103,6 +103,12 @@ export class WorkflowsSidebarTab extends SidebarTab {
|
||||
.allInnerTexts()
|
||||
}
|
||||
|
||||
async getActiveWorkflowName() {
|
||||
return await this.page
|
||||
.locator('.comfyui-workflows-open .p-tree-node-selected .node-label')
|
||||
.innerText()
|
||||
}
|
||||
|
||||
async getTopLevelSavedWorkflowNames() {
|
||||
return await this.page
|
||||
.locator('.comfyui-workflows-browse .node-label')
|
||||
|
||||
@@ -77,8 +77,13 @@ test.describe('Group Node', () => {
|
||||
.click()
|
||||
})
|
||||
})
|
||||
|
||||
test('Can be added to canvas using search', async ({ comfyPage }) => {
|
||||
// The 500ms fixed delay on the search results is causing flakiness
|
||||
// Potential solution: add a spinner state when the search is in progress,
|
||||
// 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'
|
||||
await comfyPage.convertAllNodesToGroupNode(groupNodeName)
|
||||
await comfyPage.doubleClickCanvas()
|
||||
|
||||
@@ -320,6 +320,15 @@ test.describe('Node Interaction', () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('group-selected-nodes.png')
|
||||
})
|
||||
|
||||
test('Can fit group to contents', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('oversized_group')
|
||||
await comfyPage.ctrlA()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('group-fit-to-contents.png')
|
||||
})
|
||||
|
||||
// Somehow this test fails on GitHub Actions. It works locally.
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/pull/736
|
||||
test.skip('Can pin/unpin nodes with keyboard shortcut', async ({
|
||||
@@ -537,6 +546,34 @@ test.describe('Load workflow', () => {
|
||||
await comfyPage.loadWorkflow('string_input')
|
||||
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', () => {
|
||||
|
||||
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 100 KiB |
@@ -379,7 +379,9 @@ test.describe('Menu', () => {
|
||||
// Open the sidebar
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
await tab.open()
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setupWorkflowsDirectory({})
|
||||
})
|
||||
|
||||
@@ -392,7 +394,7 @@ test.describe('Menu', () => {
|
||||
await tab.newBlankWorkflowButton.click()
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json',
|
||||
'Unsaved Workflow (2).json'
|
||||
'*Unsaved Workflow (2).json'
|
||||
])
|
||||
})
|
||||
|
||||
@@ -450,6 +452,44 @@ test.describe('Menu', () => {
|
||||
).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')
|
||||
await comfyPage.nextFrame()
|
||||
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 ({
|
||||
comfyPage
|
||||
}) => {
|
||||
@@ -475,12 +515,12 @@ test.describe('Menu', () => {
|
||||
`tempWorkflow-${test.info().title}`
|
||||
)
|
||||
const closeButton = comfyPage.page.locator(
|
||||
'.comfyui-workflows-open .p-button-icon.pi-times'
|
||||
'.comfyui-workflows-open .close-workflow-button'
|
||||
)
|
||||
await closeButton.click()
|
||||
expect(
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
).toEqual(['Unsaved Workflow.json'])
|
||||
).toEqual(['*Unsaved Workflow.json'])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ test.describe('Canvas Right Click Menu', () => {
|
||||
await dialog.accept('GroupNode2CLIP')
|
||||
})
|
||||
await comfyPage.rightClickCanvas()
|
||||
await comfyPage.page.getByText('Convert to Group Node').click()
|
||||
await comfyPage.clickContextMenuItem('Convert to Group Node')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'right-click-node-group-node.png'
|
||||
|
||||
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 96 KiB |
@@ -14,6 +14,7 @@ const jestConfig: JestConfigWithTsJest = {
|
||||
}
|
||||
]
|
||||
},
|
||||
transformIgnorePatterns: ['/node_modules/(?!(three|@three)/)'],
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
'\\.(css|less|scss|sass)$': 'identity-obj-proxy'
|
||||
|
||||
13
lint-staged.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
export default {
|
||||
'./**/*.js': (stagedFiles) => formatFiles(stagedFiles),
|
||||
|
||||
'./**/*.{ts,tsx,vue}': (stagedFiles) => [
|
||||
...formatFiles(stagedFiles),
|
||||
'tsc --noEmit',
|
||||
'tsc-strict'
|
||||
]
|
||||
}
|
||||
|
||||
function formatFiles(fileNames) {
|
||||
return [`prettier --write ${fileNames.join(' ')}`]
|
||||
}
|
||||
121
package-lock.json
generated
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "comfyui-frontend",
|
||||
"version": "1.3.43",
|
||||
"version": "1.4.9",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "comfyui-frontend",
|
||||
"version": "1.3.43",
|
||||
"version": "1.4.9",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.2.16",
|
||||
"@comfyorg/litegraph": "^0.8.26",
|
||||
"@comfyorg/comfyui-electron-types": "^0.3.6",
|
||||
"@comfyorg/litegraph": "^0.8.35",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
@@ -24,6 +24,7 @@
|
||||
"pinia": "^2.1.7",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.0.5",
|
||||
"three": "^0.170.0",
|
||||
"vue": "^3.4.31",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.4.3",
|
||||
@@ -40,6 +41,7 @@
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "^20.14.8",
|
||||
"@types/three": "^0.169.0",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vue/vue3-jest": "^29.2.6",
|
||||
@@ -63,7 +65,6 @@
|
||||
"tailwindcss": "^3.4.4",
|
||||
"ts-jest": "^29.1.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-files": "^1.1.4",
|
||||
"tsx": "^4.15.6",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^8.0.0",
|
||||
@@ -1916,15 +1917,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@comfyorg/comfyui-electron-types": {
|
||||
"version": "0.2.16",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.2.16.tgz",
|
||||
"integrity": "sha512-Hm6NeyMK4sd2V5AyOnvfI+tvCsXr5NBG8wOZlWyyD17ADpbQnpm6qPMWzvm4vCp/YvTR7cUbDGiY0quhofuQGg==",
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.3.6.tgz",
|
||||
"integrity": "sha512-wgMgESnCcRzvVkk8CwWiTAUJxC4LBvw5uTENxzaWkEL0qrnmiGrVLore00yX3cYz04hJaTA6PqasLqgVLDOenw==",
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.8.26",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.26.tgz",
|
||||
"integrity": "sha512-q0Vcd5usphR5nghfyFksVx+VM+eSB1MyX8Ne304KFDnr214KQMA6DAjrEQJlGBUUCybLiOtPCvd3dxPecEQiSQ==",
|
||||
"version": "0.8.35",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.35.tgz",
|
||||
"integrity": "sha512-taxjPoNJLajZa3z3JSxwgArRIi5lYy3nlkmemup8bo0AtC7QpKOOE+xQ5wtSXcSMZZMxbsgQHp7FoBTeIUHngA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
@@ -2459,9 +2460,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz",
|
||||
"integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==",
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz",
|
||||
"integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -3850,6 +3851,13 @@
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"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": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -3985,6 +3993,13 @@
|
||||
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
|
||||
"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": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||
@@ -3999,6 +4014,21 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
|
||||
@@ -4011,6 +4041,13 @@
|
||||
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
|
||||
"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": {
|
||||
"version": "17.0.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
|
||||
@@ -4636,6 +4673,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
|
||||
@@ -5807,10 +5851,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
@@ -6778,6 +6823,13 @@
|
||||
"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": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
@@ -10067,11 +10119,19 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
|
||||
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
|
||||
"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": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
@@ -11952,6 +12012,12 @@
|
||||
"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": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
@@ -12170,19 +12236,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz",
|
||||
|
||||
13
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.3.43",
|
||||
"version": "1.4.9",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -35,6 +35,7 @@
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "^20.14.8",
|
||||
"@types/three": "^0.169.0",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vue/vue3-jest": "^29.2.6",
|
||||
@@ -58,7 +59,6 @@
|
||||
"tailwindcss": "^3.4.4",
|
||||
"ts-jest": "^29.1.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-files": "^1.1.4",
|
||||
"tsx": "^4.15.6",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^8.0.0",
|
||||
@@ -72,8 +72,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.2.16",
|
||||
"@comfyorg/litegraph": "^0.8.26",
|
||||
"@comfyorg/comfyui-electron-types": "^0.3.6",
|
||||
"@comfyorg/litegraph": "^0.8.35",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
@@ -87,14 +87,11 @@
|
||||
"pinia": "^2.1.7",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.0.5",
|
||||
"three": "^0.170.0",
|
||||
"vue": "^3.4.31",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.4.3",
|
||||
"zod": "^3.23.8",
|
||||
"zod-validation-error": "^3.3.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"./**/*.{js,ts,tsx,vue}": "prettier --write",
|
||||
"**/*.ts": "tsc-files --noEmit"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
public/cursor/colorSelect.png
Normal file
|
After Width: | Height: | Size: 373 B |
BIN
public/cursor/paintBucket.png
Normal file
|
After Width: | Height: | Size: 410 B |
@@ -26,7 +26,10 @@ const betaMenuEnabled = computed(
|
||||
|
||||
const workflowStore = useWorkflowStore()
|
||||
const isUnsavedText = computed(() =>
|
||||
workflowStore.activeWorkflow?.isModified ? ' *' : ''
|
||||
workflowStore.activeWorkflow?.isModified ||
|
||||
!workflowStore.activeWorkflow?.isPersisted
|
||||
? ' *'
|
||||
: ''
|
||||
)
|
||||
const workflowNameText = computed(() => {
|
||||
const workflowName = workflowStore.activeWorkflow?.filename
|
||||
|
||||
@@ -125,30 +125,45 @@ const adjustMenuPosition = () => {
|
||||
const menuWidth = panelRef.value.offsetWidth
|
||||
const menuHeight = panelRef.value.offsetHeight
|
||||
|
||||
// Calculate the distance from each edge
|
||||
// Calculate distances to all edges
|
||||
const distanceLeft = lastDragState.value.x
|
||||
const distanceRight =
|
||||
lastDragState.value.windowWidth - (lastDragState.value.x + menuWidth)
|
||||
const distanceTop = lastDragState.value.y
|
||||
const distanceBottom =
|
||||
lastDragState.value.windowHeight - (lastDragState.value.y + menuHeight)
|
||||
|
||||
// Determine if the menu is closer to right/bottom or left/top
|
||||
const anchorRight = distanceRight < lastDragState.value.x
|
||||
const anchorBottom = distanceBottom < lastDragState.value.y
|
||||
// Find the smallest distance to determine which edge to anchor to
|
||||
const distances = [
|
||||
{ edge: 'left', distance: distanceLeft },
|
||||
{ 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 new position
|
||||
if (anchorRight) {
|
||||
x.value =
|
||||
screenWidth - (lastDragState.value.windowWidth - lastDragState.value.x)
|
||||
} else {
|
||||
x.value = lastDragState.value.x
|
||||
}
|
||||
// Calculate vertical position as a percentage of screen height
|
||||
const verticalRatio =
|
||||
lastDragState.value.y / lastDragState.value.windowHeight
|
||||
const horizontalRatio =
|
||||
lastDragState.value.x / lastDragState.value.windowWidth
|
||||
|
||||
if (anchorBottom) {
|
||||
y.value =
|
||||
screenHeight -
|
||||
(lastDragState.value.windowHeight - lastDragState.value.y)
|
||||
// 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 {
|
||||
y.value = lastDragState.value.y
|
||||
// bottom
|
||||
x.value = horizontalRatio * screenWidth
|
||||
y.value = screenHeight - menuHeight - closestEdge.distance // Maintain exact distance from bottom
|
||||
}
|
||||
|
||||
// Ensure the menu stays within the screen bounds
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
<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>
|
||||
30
src/components/bottomPanel/tabs/terminal/BaseTerminal.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<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>
|
||||
73
src/components/bottomPanel/tabs/terminal/CommandTerminal.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<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>
|
||||
90
src/components/bottomPanel/tabs/terminal/LogsTerminal.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<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>
|
||||
@@ -28,7 +28,14 @@
|
||||
class="flex flex-row items-center gap-2"
|
||||
v-if="status === 'in_progress' || status === 'paused'"
|
||||
>
|
||||
<ProgressBar class="flex-1" :value="downloadProgress" />
|
||||
<!-- 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
|
||||
class="flex-1"
|
||||
:value="downloadProgress"
|
||||
:show-value="downloadProgress > 10"
|
||||
/>
|
||||
|
||||
<Button
|
||||
class="file-action-button"
|
||||
|
||||
102
src/components/common/FormItem.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<!-- A generalized form item for rendering in a form. -->
|
||||
<template>
|
||||
<div class="form-label flex flex-grow items-center">
|
||||
<span class="text-[var(--p-text-muted-color)]">
|
||||
<slot name="name-prefix"></slot>
|
||||
{{ props.item.name }}
|
||||
<i
|
||||
v-if="props.item.tooltip"
|
||||
class="pi pi-info-circle bg-transparent"
|
||||
v-tooltip="props.item.tooltip"
|
||||
/>
|
||||
<slot name="name-suffix"></slot>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-input flex justify-end">
|
||||
<component
|
||||
:is="markRaw(getFormComponent(props.item))"
|
||||
:id="props.id"
|
||||
v-model:modelValue="formValue"
|
||||
v-bind="getFormAttrs(props.item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FormItem } from '@/types/settingTypes'
|
||||
import { markRaw, type Component } from 'vue'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import Select from 'primevue/select'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import CustomFormValue from '@/components/common/CustomFormValue.vue'
|
||||
import InputSlider from '@/components/common/InputSlider.vue'
|
||||
|
||||
const formValue = defineModel<any>('formValue')
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
item: FormItem
|
||||
id: string | undefined
|
||||
}>(),
|
||||
{
|
||||
id: undefined
|
||||
}
|
||||
)
|
||||
|
||||
function getFormAttrs(item: FormItem) {
|
||||
const attrs = { ...(item.attrs || {}) }
|
||||
const inputType = item.type
|
||||
if (typeof inputType === 'function') {
|
||||
attrs['renderFunction'] = () =>
|
||||
inputType(
|
||||
props.item.name,
|
||||
(v: any) => (formValue.value = v),
|
||||
formValue.value,
|
||||
item.attrs
|
||||
)
|
||||
}
|
||||
switch (item.type) {
|
||||
case 'combo':
|
||||
attrs['options'] =
|
||||
typeof item.options === 'function'
|
||||
? item.options(formValue.value)
|
||||
: item.options
|
||||
if (typeof item.options[0] !== 'string') {
|
||||
attrs['optionLabel'] = 'text'
|
||||
attrs['optionValue'] = 'value'
|
||||
}
|
||||
break
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
function getFormComponent(item: FormItem): Component {
|
||||
if (typeof item.type === 'function') {
|
||||
return CustomFormValue
|
||||
}
|
||||
switch (item.type) {
|
||||
case 'boolean':
|
||||
return ToggleSwitch
|
||||
case 'number':
|
||||
return InputNumber
|
||||
case 'slider':
|
||||
return InputSlider
|
||||
case 'combo':
|
||||
return Select
|
||||
default:
|
||||
return InputText
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-input :deep(.input-slider) .p-inputnumber input,
|
||||
.form-input :deep(.input-slider) .slider-part {
|
||||
@apply w-20;
|
||||
}
|
||||
|
||||
.form-input :deep(.p-inputtext),
|
||||
.form-input :deep(.p-select) {
|
||||
@apply w-44;
|
||||
}
|
||||
</style>
|
||||
@@ -13,6 +13,7 @@
|
||||
:modelValue="modelValue"
|
||||
@update:modelValue="updateValue"
|
||||
class="input-part"
|
||||
:max-fraction-digits="3"
|
||||
:class="inputClass"
|
||||
:min="min"
|
||||
:max="max"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Tree
|
||||
class="tree-explorer p-2 2xl:p-4"
|
||||
class="tree-explorer py-0 px-2 2xl:px-4"
|
||||
:class="props.class"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
v-model:selectionKeys="selectionKeys"
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
</template>
|
||||
|
||||
<div class="action-container">
|
||||
<ReportIssueButton v-if="showSendError" :error="props.error" />
|
||||
<FindIssueButton
|
||||
:errorMessage="props.error.exception_message"
|
||||
:repoOwner="repoOwner"
|
||||
@@ -44,9 +45,11 @@ import Divider from 'primevue/divider'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import FindIssueButton from '@/components/dialog/content/error/FindIssueButton.vue'
|
||||
import ReportIssueButton from '@/components/dialog/content/error/ReportIssueButton.vue'
|
||||
import type { ExecutionErrorWsMessage, SystemStats } from '@/types/apiTypes'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
error: ExecutionErrorWsMessage
|
||||
@@ -59,6 +62,7 @@ const reportOpen = ref(false)
|
||||
const showReport = () => {
|
||||
reportOpen.value = true
|
||||
}
|
||||
const showSendError = isElectron()
|
||||
|
||||
const toast = useToast()
|
||||
const { copy, isSupported } = useClipboard()
|
||||
|
||||
@@ -71,6 +71,14 @@
|
||||
</template>
|
||||
</Suspense>
|
||||
</TabPanel>
|
||||
<TabPanel key="server-config" value="Server-Config">
|
||||
<Suspense>
|
||||
<ServerConfigPanel />
|
||||
<template #fallback>
|
||||
<div>Loading server config panel...</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</ScrollPanel>
|
||||
@@ -93,6 +101,7 @@ import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import { flattenTree } from '@/utils/treeUtil'
|
||||
import AboutPanel from './setting/AboutPanel.vue'
|
||||
import FirstTimeUIMessage from './setting/FirstTimeUIMessage.vue'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
const KeybindingPanel = defineAsyncComponent(
|
||||
() => import('./setting/KeybindingPanel.vue')
|
||||
@@ -100,6 +109,9 @@ const KeybindingPanel = defineAsyncComponent(
|
||||
const ExtensionPanel = defineAsyncComponent(
|
||||
() => import('./setting/ExtensionPanel.vue')
|
||||
)
|
||||
const ServerConfigPanel = defineAsyncComponent(
|
||||
() => import('./setting/ServerConfigPanel.vue')
|
||||
)
|
||||
|
||||
interface ISettingGroup {
|
||||
label: string
|
||||
@@ -124,18 +136,33 @@ const extensionPanelNode: SettingTreeNode = {
|
||||
children: []
|
||||
}
|
||||
|
||||
const serverConfigPanelNode: SettingTreeNode = {
|
||||
key: 'server-config',
|
||||
label: 'Server-Config',
|
||||
children: []
|
||||
}
|
||||
|
||||
const extensionPanelNodeList = computed<SettingTreeNode[]>(() => {
|
||||
const settingStore = useSettingStore()
|
||||
const showExtensionPanel = settingStore.get('Comfy.Settings.ExtensionPanel')
|
||||
return showExtensionPanel ? [extensionPanelNode] : []
|
||||
})
|
||||
|
||||
/**
|
||||
* Server config panel is only available in Electron. We might want to support
|
||||
* it in the web version in the future.
|
||||
*/
|
||||
const serverConfigPanelNodeList = computed<SettingTreeNode[]>(() => {
|
||||
return isElectron() ? [serverConfigPanelNode] : []
|
||||
})
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const settingRoot = computed<SettingTreeNode>(() => settingStore.settingTree)
|
||||
const categories = computed<SettingTreeNode[]>(() => [
|
||||
...(settingRoot.value.children || []),
|
||||
keybindingPanelNode,
|
||||
...extensionPanelNodeList.value,
|
||||
...serverConfigPanelNodeList.value,
|
||||
aboutPanelNode
|
||||
])
|
||||
const activeCategory = ref<SettingTreeNode | null>(null)
|
||||
|
||||
51
src/components/dialog/content/error/ReportIssueButton.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<Button
|
||||
@click="reportIssue"
|
||||
:label="$t('reportIssue')"
|
||||
:severity="submitted ? 'success' : 'secondary'"
|
||||
:icon="icon"
|
||||
:disabled="submitted"
|
||||
v-tooltip="$t('reportIssueTooltip')"
|
||||
>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, defineProps } from 'vue'
|
||||
import Button from 'primevue/button'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { ExecutionErrorWsMessage } from '@/types/apiTypes'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
|
||||
const { error } = defineProps<{
|
||||
error: ExecutionErrorWsMessage
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const submitting = ref(false)
|
||||
const submitted = ref(false)
|
||||
const icon = computed(
|
||||
() => `pi ${submitting.value ? 'pi-spin pi-spinner' : 'pi-send'}`
|
||||
)
|
||||
|
||||
const reportIssue = async () => {
|
||||
if (submitting.value) return
|
||||
submitting.value = true
|
||||
try {
|
||||
await electronAPI().sendErrorToSentry(error.exception_message, {
|
||||
stackTrace: error.traceback?.join('\n'),
|
||||
nodeType: error.node_type
|
||||
})
|
||||
submitted.value = true
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('reportSent'),
|
||||
life: 3000
|
||||
})
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
43
src/components/dialog/content/setting/ServerConfigPanel.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div
|
||||
v-for="([label, items], i) in Object.entries(serverConfigsByCategory)"
|
||||
:key="label"
|
||||
>
|
||||
<Divider v-if="i > 0" />
|
||||
<h3>{{ formatCamelCase(label) }}</h3>
|
||||
<div v-for="item in items" :key="item.name" class="flex items-center mb-4">
|
||||
<FormItem :item="item" v-model:formValue="item.value" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Divider from 'primevue/divider'
|
||||
import FormItem from '@/components/common/FormItem.vue'
|
||||
import { formatCamelCase } from '@/utils/formatUtil'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useServerConfigStore } from '@/stores/serverConfigStore'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, watch } from 'vue'
|
||||
import { SERVER_CONFIG_ITEMS } from '@/constants/serverConfig'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const serverConfigStore = useServerConfigStore()
|
||||
const { serverConfigsByCategory, launchArgs, serverConfigValues } =
|
||||
storeToRefs(serverConfigStore)
|
||||
|
||||
onMounted(() => {
|
||||
serverConfigStore.loadServerConfig(
|
||||
SERVER_CONFIG_ITEMS,
|
||||
settingStore.get('Comfy.Server.ServerConfigValues')
|
||||
)
|
||||
})
|
||||
|
||||
watch(launchArgs, (newVal) => {
|
||||
settingStore.set('Comfy.Server.LaunchArgs', newVal)
|
||||
})
|
||||
|
||||
watch(serverConfigValues, (newVal) => {
|
||||
settingStore.set('Comfy.Server.ServerConfigValues', newVal)
|
||||
})
|
||||
</script>
|
||||
@@ -7,45 +7,15 @@
|
||||
:key="setting.id"
|
||||
class="setting-item flex items-center mb-4"
|
||||
>
|
||||
<div class="setting-label flex flex-grow items-center">
|
||||
<span class="text-[var(--p-text-muted-color)]">
|
||||
<Tag v-if="setting.experimental" :value="$t('experimental')" />
|
||||
<Tag
|
||||
v-if="setting.deprecated"
|
||||
:value="$t('deprecated')"
|
||||
severity="danger" />
|
||||
{{ setting.name }}
|
||||
<i
|
||||
v-if="setting.tooltip"
|
||||
class="pi pi-info-circle bg-transparent"
|
||||
v-tooltip="setting.tooltip"
|
||||
/></span>
|
||||
</div>
|
||||
<div class="setting-input flex justify-end">
|
||||
<component
|
||||
:is="markRaw(getSettingComponent(setting))"
|
||||
:id="setting.id"
|
||||
:modelValue="settingStore.get(setting.id)"
|
||||
@update:modelValue="updateSetting(setting, $event)"
|
||||
v-bind="getSettingAttrs(setting)"
|
||||
/>
|
||||
</div>
|
||||
<SettingItem :setting="setting" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { SettingParams } from '@/types/settingTypes'
|
||||
import { markRaw, type Component } from 'vue'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import Select from 'primevue/select'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import Divider from 'primevue/divider'
|
||||
import Tag from 'primevue/tag'
|
||||
import CustomSettingValue from '@/components/dialog/content/setting/CustomSettingValue.vue'
|
||||
import InputSlider from '@/components/common/InputSlider.vue'
|
||||
import SettingItem from '@/components/dialog/content/setting/SettingItem.vue'
|
||||
import { SettingParams } from '@/types/settingTypes'
|
||||
import { formatCamelCase } from '@/utils/formatUtil'
|
||||
|
||||
defineProps<{
|
||||
@@ -55,67 +25,4 @@ defineProps<{
|
||||
}
|
||||
divider?: boolean
|
||||
}>()
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
function getSettingAttrs(setting: SettingParams) {
|
||||
const attrs = { ...(setting.attrs || {}) }
|
||||
const settingType = setting.type
|
||||
if (typeof settingType === 'function') {
|
||||
attrs['renderFunction'] = () =>
|
||||
settingType(
|
||||
setting.name,
|
||||
(v) => updateSetting(setting, v),
|
||||
settingStore.get(setting.id),
|
||||
setting.attrs
|
||||
)
|
||||
}
|
||||
switch (setting.type) {
|
||||
case 'combo':
|
||||
attrs['options'] =
|
||||
typeof setting.options === 'function'
|
||||
? setting.options(settingStore.get(setting.id))
|
||||
: setting.options
|
||||
if (typeof setting.options[0] !== 'string') {
|
||||
attrs['optionLabel'] = 'text'
|
||||
attrs['optionValue'] = 'value'
|
||||
}
|
||||
break
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
const updateSetting = (setting: SettingParams, value: any) => {
|
||||
settingStore.set(setting.id, value)
|
||||
}
|
||||
|
||||
function getSettingComponent(setting: SettingParams): Component {
|
||||
if (typeof setting.type === 'function') {
|
||||
return CustomSettingValue
|
||||
}
|
||||
switch (setting.type) {
|
||||
case 'boolean':
|
||||
return ToggleSwitch
|
||||
case 'number':
|
||||
return InputNumber
|
||||
case 'slider':
|
||||
return InputSlider
|
||||
case 'combo':
|
||||
return Select
|
||||
default:
|
||||
return InputText
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.setting-input :deep(.input-slider) .p-inputnumber input,
|
||||
.setting-input :deep(.input-slider) .slider-part {
|
||||
@apply w-20;
|
||||
}
|
||||
|
||||
.setting-input :deep(.p-inputtext),
|
||||
.setting-input :deep(.p-select) {
|
||||
@apply w-44;
|
||||
}
|
||||
</style>
|
||||
|
||||
35
src/components/dialog/content/setting/SettingItem.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<FormItem
|
||||
:item="setting"
|
||||
:id="setting.id"
|
||||
:formValue="settingValue"
|
||||
@update:formValue="updateSettingValue"
|
||||
>
|
||||
<template #name-prefix>
|
||||
<Tag v-if="setting.experimental" :value="$t('experimental')" />
|
||||
<Tag
|
||||
v-if="setting.deprecated"
|
||||
:value="$t('deprecated')"
|
||||
severity="danger"
|
||||
/>
|
||||
</template>
|
||||
</FormItem>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Tag from 'primevue/tag'
|
||||
import FormItem from '@/components/common/FormItem.vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { SettingParams } from '@/types/settingTypes'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
setting: SettingParams
|
||||
}>()
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const settingValue = computed(() => settingStore.get(props.setting.id))
|
||||
const updateSettingValue = (value: any) => {
|
||||
settingStore.set(props.setting.id, value)
|
||||
}
|
||||
</script>
|
||||
@@ -47,7 +47,8 @@ import {
|
||||
DragAndScale,
|
||||
LGraphCanvas,
|
||||
ContextMenu,
|
||||
LGraphBadge
|
||||
LGraphBadge,
|
||||
CanvasPointer
|
||||
} from '@comfyorg/litegraph'
|
||||
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
@@ -61,6 +62,7 @@ import { usePragmaticDroppable } from '@/hooks/dndHooks'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { setStorageValue } from '@/scripts/utils'
|
||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
const emit = defineEmits(['ready'])
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
@@ -161,6 +163,28 @@ 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(() => {
|
||||
LiteGraph.alwaysSnapToGrid = settingStore.get('pysssss.SnapToGrid')
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (!canvasStore.canvas) return
|
||||
|
||||
@@ -178,13 +202,26 @@ watchEffect(() => {
|
||||
})
|
||||
|
||||
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(() => {
|
||||
if (workflowStore.activeWorkflow) {
|
||||
const workflow = workflowStore.activeWorkflow
|
||||
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, {
|
||||
onDrop: (event) => {
|
||||
const loc = event.location.current.input
|
||||
@@ -262,6 +299,7 @@ onMounted(async () => {
|
||||
ChangeTracker.init(comfyApp)
|
||||
await comfyApp.setup(canvasRef.value)
|
||||
canvasStore.canvas = comfyApp.canvas
|
||||
canvasStore.canvas.render_canvas_border = false
|
||||
workspaceStore.spinner = false
|
||||
|
||||
window['app'] = comfyApp
|
||||
|
||||
@@ -85,12 +85,14 @@ onMounted(async () => {
|
||||
appData.value = paths.appData
|
||||
appPath.value = paths.appPath
|
||||
installPath.value = paths.defaultInstallPath
|
||||
|
||||
await validatePath(paths.defaultInstallPath)
|
||||
})
|
||||
|
||||
const validatePath = async () => {
|
||||
const validatePath = async (path: string) => {
|
||||
try {
|
||||
pathError.value = ''
|
||||
const validation = await electron.validateInstallPath(installPath.value)
|
||||
const validation = await electron.validateInstallPath(path)
|
||||
|
||||
if (!validation.isValid) {
|
||||
pathError.value = validation.error
|
||||
@@ -105,7 +107,7 @@ const browsePath = async () => {
|
||||
const result = await electron.showDirectoryPicker()
|
||||
if (result) {
|
||||
installPath.value = result
|
||||
await validatePath()
|
||||
await validatePath(result)
|
||||
}
|
||||
} catch (error) {
|
||||
pathError.value = t('install.failedToSelectDirectory')
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<div class="comfy-vue-node-search-container">
|
||||
<div class="comfy-vue-node-preview-container" v-if="enableNodePreview">
|
||||
<div
|
||||
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 absolute left-[-350px] top-[50px]"
|
||||
v-if="enableNodePreview"
|
||||
>
|
||||
<NodePreview
|
||||
:nodeDef="hoveredSuggestion"
|
||||
:key="hoveredSuggestion?.name || ''"
|
||||
@@ -11,10 +16,10 @@
|
||||
<Button
|
||||
icon="pi pi-filter"
|
||||
severity="secondary"
|
||||
class="_filter-button"
|
||||
class="filter-button z-10"
|
||||
@click="nodeSearchFilterVisible = true"
|
||||
/>
|
||||
<Dialog v-model:visible="nodeSearchFilterVisible" class="_dialog">
|
||||
<Dialog v-model:visible="nodeSearchFilterVisible" class="min-w-96">
|
||||
<template #header>
|
||||
<h3>Add node filter condition</h3>
|
||||
</template>
|
||||
@@ -25,7 +30,7 @@
|
||||
|
||||
<AutoCompletePlus
|
||||
:model-value="props.filters"
|
||||
class="comfy-vue-node-search-box"
|
||||
class="comfy-vue-node-search-box z-10 flex-grow"
|
||||
scrollHeight="40vh"
|
||||
:placeholder="placeholder"
|
||||
:input-id="inputId"
|
||||
@@ -148,31 +153,3 @@ const setHoverSuggestion = (index: number) => {
|
||||
hoveredSuggestion.value = value
|
||||
}
|
||||
</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>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<ElectronDownloadItems v-if="isElectron()" />
|
||||
|
||||
<TreeExplorer
|
||||
class="model-lib-tree-explorer py-0"
|
||||
class="model-lib-tree-explorer"
|
||||
:roots="renderedRoot.children"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
class="m-2"
|
||||
/>
|
||||
<TreeExplorer
|
||||
class="node-lib-tree-explorer py-0"
|
||||
class="node-lib-tree-explorer"
|
||||
:roots="renderedRoot.children"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
>
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
<template>
|
||||
<SidebarTabTemplate :title="$t('sideToolbar.queue')">
|
||||
<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
|
||||
:icon="
|
||||
imageFit === 'cover'
|
||||
@@ -99,6 +111,7 @@ import Button from 'primevue/button'
|
||||
import ConfirmPopup from 'primevue/confirmpopup'
|
||||
import ContextMenu from 'primevue/contextmenu'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import Popover from 'primevue/popover'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import TaskItem from './queue/TaskItem.vue'
|
||||
import ResultGallery from './queue/ResultGallery.vue'
|
||||
@@ -111,7 +124,9 @@ import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
const IMAGE_FIT = 'Comfy.Queue.ImageFit'
|
||||
const SETTING_FIT = 'Comfy.Queue.ImageFit'
|
||||
const SETTING_FLAT = 'Comfy.Queue.ShowFlatList'
|
||||
const SETTING_FILTER = 'Comfy.Queue.Filter'
|
||||
const confirm = useConfirm()
|
||||
const toast = useToast()
|
||||
const queueStore = useQueueStore()
|
||||
@@ -120,7 +135,7 @@ const commandStore = useCommandStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
// Expanded view: show all outputs in a flat list.
|
||||
const isExpanded = ref(false)
|
||||
const isExpanded = computed<boolean>(() => settingStore.get(SETTING_FLAT))
|
||||
const visibleTasks = ref<TaskItemImpl[]>([])
|
||||
const scrollContainer = ref<HTMLElement | null>(null)
|
||||
const loadMoreTrigger = ref<HTMLElement | null>(null)
|
||||
@@ -128,10 +143,27 @@ const galleryActiveIndex = ref(-1)
|
||||
// Folder view: only show outputs from a single selected task.
|
||||
const folderTask = ref<TaskItemImpl | null>(null)
|
||||
const isInFolderView = computed(() => folderTask.value !== null)
|
||||
const imageFit = computed<string>(() => settingStore.get(IMAGE_FIT))
|
||||
const imageFit = computed<string>(() => settingStore.get(SETTING_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 SCROLL_THRESHOLD = 100 // pixels from bottom to trigger load
|
||||
const MAX_LOAD_ITERATIONS = 10
|
||||
|
||||
const allTasks = computed(() =>
|
||||
isInFolderView.value
|
||||
@@ -149,21 +181,48 @@ const allGalleryItems = computed(() =>
|
||||
})
|
||||
)
|
||||
|
||||
const loadMoreItems = () => {
|
||||
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
|
||||
})
|
||||
|
||||
const loadMoreItems = (iteration: number) => {
|
||||
const currentLength = visibleTasks.value.length
|
||||
const newTasks = allTasks.value.slice(
|
||||
const newTasks = filterTasks(allTasks.value).slice(
|
||||
currentLength,
|
||||
currentLength + ITEMS_PER_PAGE
|
||||
)
|
||||
visibleTasks.value.push(...newTasks)
|
||||
// If we've added some items, check if we need to add more
|
||||
// Prevent loading everything at once in case of render update issues
|
||||
if (newTasks.length && iteration < MAX_LOAD_ITERATIONS) {
|
||||
nextTick(() => {
|
||||
checkAndLoadMore(iteration + 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const checkAndLoadMore = () => {
|
||||
const checkAndLoadMore = (iteration: number) => {
|
||||
if (!scrollContainer.value) return
|
||||
|
||||
const { scrollHeight, scrollTop, clientHeight } = scrollContainer.value
|
||||
if (scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD) {
|
||||
loadMoreItems()
|
||||
loadMoreItems(iteration)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +230,7 @@ useInfiniteScroll(
|
||||
scrollContainer,
|
||||
() => {
|
||||
if (visibleTasks.value.length < allTasks.value.length) {
|
||||
loadMoreItems()
|
||||
loadMoreItems(0)
|
||||
}
|
||||
},
|
||||
{ distance: SCROLL_THRESHOLD }
|
||||
@@ -181,16 +240,20 @@ useInfiniteScroll(
|
||||
// This is necessary as the sidebar tab can change size when user drags the splitter.
|
||||
useResizeObserver(scrollContainer, () => {
|
||||
nextTick(() => {
|
||||
checkAndLoadMore()
|
||||
checkAndLoadMore(0)
|
||||
})
|
||||
})
|
||||
|
||||
const updateVisibleTasks = () => {
|
||||
visibleTasks.value = allTasks.value.slice(0, ITEMS_PER_PAGE)
|
||||
visibleTasks.value = filterTasks(allTasks.value).slice(0, ITEMS_PER_PAGE)
|
||||
|
||||
nextTick(() => {
|
||||
checkAndLoadMore(0)
|
||||
})
|
||||
}
|
||||
|
||||
const toggleExpanded = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
settingStore.set(SETTING_FLAT, !isExpanded.value)
|
||||
updateVisibleTasks()
|
||||
}
|
||||
|
||||
@@ -291,7 +354,10 @@ const exitFolderView = () => {
|
||||
}
|
||||
|
||||
const toggleImageFit = () => {
|
||||
settingStore.set(IMAGE_FIT, imageFit.value === 'cover' ? 'contain' : 'cover')
|
||||
settingStore.set(
|
||||
SETTING_FIT,
|
||||
imageFit.value === 'cover' ? 'contain' : 'cover'
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -50,10 +50,13 @@
|
||||
<template #node="{ node }">
|
||||
<TreeExplorerTreeNode :node="node">
|
||||
<template #before-label="{ node }">
|
||||
<span v-if="node.data.isModified">*</span>
|
||||
<span v-if="node.data.isModified || !node.data.isPersisted"
|
||||
>*</span
|
||||
>
|
||||
</template>
|
||||
<template #actions="{ node }">
|
||||
<Button
|
||||
class="close-workflow-button"
|
||||
icon="pi pi-times"
|
||||
text
|
||||
:severity="
|
||||
|
||||
@@ -16,9 +16,13 @@
|
||||
class="mt-2 flex flex-row items-center gap-2"
|
||||
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
|
||||
class="flex-1"
|
||||
:value="Number((download.progress * 100).toFixed(1))"
|
||||
:show-value="download.progress > 0.1"
|
||||
/>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<TreeExplorer
|
||||
class="node-lib-bookmark-tree-explorer py-0"
|
||||
class="node-lib-bookmark-tree-explorer"
|
||||
ref="treeExplorerRef"
|
||||
:roots="renderedBookmarkedRoot.children"
|
||||
:expandedKeys="expandedKeys"
|
||||
|
||||
38
src/components/sidebar/tabs/queue/OutputFilters.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<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>
|
||||
@@ -31,16 +31,20 @@
|
||||
|
||||
<div class="task-item-details">
|
||||
<div class="tag-wrapper status-tag-group">
|
||||
<Tag v-if="isFlatTask && task.isHistory" class="node-name-tag">
|
||||
<Tag v-if="isFlatTask && task.isHistory && node" class="node-name-tag">
|
||||
<Button
|
||||
class="task-node-link"
|
||||
:label="`${node?.type} (#${node?.id})`"
|
||||
:label="`${node.type} (#${node.id})`"
|
||||
link
|
||||
size="small"
|
||||
@click="app.goToNode(node?.id)"
|
||||
/>
|
||||
</Tag>
|
||||
<Tag :severity="taskTagSeverity(task.displayStatus)">
|
||||
<Tag
|
||||
: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-if="task.isHistory" class="task-time">
|
||||
{{ formatTime(task.executionTimeInSeconds) }}
|
||||
@@ -90,6 +94,7 @@ const node: ComfyNode | null =
|
||||
) ?? null
|
||||
: null
|
||||
const progressPreviewBlobUrl = ref('')
|
||||
const isCachedResult = props.isFlatTask && coverResult?.cached
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
@@ -142,7 +147,7 @@ const taskStatusText = (status: TaskItemDisplayStatus) => {
|
||||
case TaskItemDisplayStatus.Running:
|
||||
return '<i class="pi pi-spin pi-spinner" style="font-weight: bold"></i> Running'
|
||||
case TaskItemDisplayStatus.Completed:
|
||||
return '<i class="pi pi-check" style="font-weight: bold"></i>'
|
||||
return `<i class="pi pi-check${isCachedResult ? ' cached' : ''}" style="font-weight: bold"></i>`
|
||||
case TaskItemDisplayStatus.Failed:
|
||||
return 'Failed'
|
||||
case TaskItemDisplayStatus.Cancelled:
|
||||
@@ -226,4 +231,15 @@ are floating on top of images. */
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.task-cached-icon {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
:deep(.pi-check.cached) {
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
<div class="relative">
|
||||
<span
|
||||
class="status-indicator"
|
||||
v-if="!workspaceStore.shiftDown && option.workflow.isModified"
|
||||
v-if="
|
||||
!workspaceStore.shiftDown &&
|
||||
(option.workflow.isModified || !option.workflow.isPersisted)
|
||||
"
|
||||
>•</span
|
||||
>
|
||||
<Button
|
||||
|
||||
@@ -59,7 +59,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
key: 's',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Comfy.ExportWorkflow'
|
||||
commandId: 'Comfy.SaveWorkflow'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
@@ -74,13 +74,6 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
},
|
||||
commandId: 'Comfy.ClearWorkflow'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'd',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Comfy.LoadDefaultWorkflow'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'g',
|
||||
@@ -173,7 +166,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
key: '`',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Workspace.ToggleBottomPanelTab.integrated-terminal'
|
||||
commandId: 'Workspace.ToggleBottomPanelTab.logs-terminal'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { Keybinding } from '@/types/keyBindingTypes'
|
||||
import { NodeBadgeMode } from '@/types/nodeSource'
|
||||
import {
|
||||
LinkReleaseTriggerAction,
|
||||
LinkReleaseTriggerMode
|
||||
} from '@/types/searchBoxTypes'
|
||||
import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'
|
||||
import type { SettingParams } from '@/types/settingTypes'
|
||||
import { LinkMarkerShape } from '@comfyorg/litegraph'
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
@@ -24,17 +21,9 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
options: ['default', 'litegraph (legacy)'],
|
||||
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',
|
||||
category: ['LiteGraph', 'LinkRelease', 'Action'],
|
||||
name: 'Action on link release (No modifier)',
|
||||
type: 'combo',
|
||||
options: Object.values(LinkReleaseTriggerAction),
|
||||
@@ -42,6 +31,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.LinkRelease.ActionShift',
|
||||
category: ['LiteGraph', 'LinkRelease', 'ActionShift'],
|
||||
name: 'Action on link release (Shift)',
|
||||
type: 'combo',
|
||||
options: Object.values(LinkReleaseTriggerAction),
|
||||
@@ -81,7 +71,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Sidebar.Location',
|
||||
category: ['Comfy', 'Sidebar', 'Location'],
|
||||
category: ['Appearance', 'Sidebar', 'Location'],
|
||||
name: 'Sidebar location',
|
||||
type: 'combo',
|
||||
options: ['left', 'right'],
|
||||
@@ -89,7 +79,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Sidebar.Size',
|
||||
category: ['Comfy', 'Sidebar', 'Size'],
|
||||
category: ['Appearance', 'Sidebar', 'Size'],
|
||||
name: 'Sidebar size',
|
||||
type: 'combo',
|
||||
options: ['normal', 'small'],
|
||||
@@ -97,7 +87,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.TextareaWidget.FontSize',
|
||||
category: ['Comfy', 'Node Widget', 'TextareaWidget', 'FontSize'],
|
||||
category: ['Appearance', 'Node Widget', 'TextareaWidget', 'FontSize'],
|
||||
name: 'Textarea widget font size',
|
||||
type: 'slider',
|
||||
defaultValue: 10,
|
||||
@@ -121,7 +111,8 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.CanvasInfo',
|
||||
name: 'Show canvas info (fps, etc.)',
|
||||
category: ['LiteGraph', 'Canvas', 'CanvasInfo'],
|
||||
name: 'Show canvas info on bottom left corner (fps, etc.)',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
},
|
||||
@@ -143,6 +134,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Node.Opacity',
|
||||
category: ['Appearance', 'Node', 'Opacity'],
|
||||
name: 'Node opacity',
|
||||
type: 'slider',
|
||||
defaultValue: 1,
|
||||
@@ -167,6 +159,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.ZoomSpeed',
|
||||
category: ['LiteGraph', 'Canvas', 'ZoomSpeed'],
|
||||
name: 'Canvas zoom speed',
|
||||
type: 'slider',
|
||||
defaultValue: 1.1,
|
||||
@@ -205,8 +198,28 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
type: 'hidden',
|
||||
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',
|
||||
category: ['LiteGraph', 'Group', 'Padding'],
|
||||
name: 'Group selected nodes padding',
|
||||
type: 'slider',
|
||||
defaultValue: 10,
|
||||
@@ -217,12 +230,14 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Node.DoubleClickTitleToEdit',
|
||||
category: ['LiteGraph', 'Node', 'DoubleClickTitleToEdit'],
|
||||
name: 'Double click node title to edit',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Group.DoubleClickTitleToEdit',
|
||||
category: ['LiteGraph', 'Group', 'DoubleClickTitleToEdit'],
|
||||
name: 'Double click group title to edit',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
@@ -270,6 +285,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.NodeBadge.NodeSourceBadgeMode',
|
||||
category: ['LiteGraph', 'Node', 'NodeSourceBadgeMode'],
|
||||
name: 'Node source badge mode',
|
||||
type: 'combo',
|
||||
options: Object.values(NodeBadgeMode),
|
||||
@@ -277,6 +293,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.NodeBadge.NodeIdBadgeMode',
|
||||
category: ['LiteGraph', 'Node', 'NodeIdBadgeMode'],
|
||||
name: 'Node ID badge mode',
|
||||
type: 'combo',
|
||||
options: [NodeBadgeMode.None, NodeBadgeMode.ShowAll],
|
||||
@@ -284,6 +301,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.NodeBadge.NodeLifeCycleBadgeMode',
|
||||
category: ['LiteGraph', 'Node', 'NodeLifeCycleBadgeMode'],
|
||||
name: 'Node life cycle badge mode',
|
||||
type: 'combo',
|
||||
options: [NodeBadgeMode.None, NodeBadgeMode.ShowAll],
|
||||
@@ -316,7 +334,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
*/
|
||||
{
|
||||
id: 'Comfy.PreviewFormat',
|
||||
category: ['Comfy', 'Node Widget', 'PreviewFormat'],
|
||||
category: ['LiteGraph', 'Node Widget', 'PreviewFormat'],
|
||||
name: 'Preview image format',
|
||||
tooltip:
|
||||
'When displaying a preview in the image widget, convert it to a lightweight image, e.g. webp, jpeg, webp;50, etc.',
|
||||
@@ -325,14 +343,14 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.DisableSliders',
|
||||
category: ['Comfy', 'Node Widget', 'DisableSliders'],
|
||||
category: ['LiteGraph', 'Node Widget', 'DisableSliders'],
|
||||
name: 'Disable node widget sliders',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'Comfy.DisableFloatRounding',
|
||||
category: ['Comfy', 'Node Widget', 'DisableFloatRounding'],
|
||||
category: ['LiteGraph', 'Node Widget', 'DisableFloatRounding'],
|
||||
name: 'Disable default float widget rounding.',
|
||||
tooltip:
|
||||
'(requires page reload) Cannot disable round when round is set by the node in the backend.',
|
||||
@@ -341,7 +359,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.FloatRoundingPrecision',
|
||||
category: ['Comfy', 'Node Widget', 'FloatRoundingPrecision'],
|
||||
category: ['LiteGraph', 'Node Widget', 'FloatRoundingPrecision'],
|
||||
name: 'Float widget rounding decimal places [0 = auto].',
|
||||
tooltip: '(requires page reload)',
|
||||
type: 'slider',
|
||||
@@ -354,7 +372,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.EnableTooltips',
|
||||
category: ['Comfy', 'Node', 'EnableTooltips'],
|
||||
category: ['LiteGraph', 'Node', 'EnableTooltips'],
|
||||
name: 'Enable Tooltips',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
@@ -395,6 +413,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.CanvasMenu',
|
||||
category: ['LiteGraph', 'Canvas', 'CanvasMenu'],
|
||||
name: 'Show graph canvas menu',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
@@ -448,7 +467,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.LinkRenderMode',
|
||||
category: ['Comfy', 'Graph', 'LinkRenderMode'],
|
||||
category: ['LiteGraph', 'Graph', 'LinkRenderMode'],
|
||||
name: 'Link Render Mode',
|
||||
defaultValue: 2,
|
||||
type: 'combo',
|
||||
@@ -461,6 +480,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Node.AutoSnapLinkToSlot',
|
||||
category: ['LiteGraph', 'Node', 'AutoSnapLinkToSlot'],
|
||||
name: 'Auto snap link to node slot',
|
||||
tooltip:
|
||||
'When dragging a link over a node, the link automatically snap to a viable input slot on the node',
|
||||
@@ -470,6 +490,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Node.SnapHighlightsNode',
|
||||
category: ['LiteGraph', 'Node', 'SnapHighlightsNode'],
|
||||
name: 'Snap highlights node',
|
||||
tooltip:
|
||||
'When dragging a link over a node with viable input slot, highlight the node',
|
||||
@@ -479,6 +500,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Node.BypassAllLinksOnDelete',
|
||||
category: ['LiteGraph', 'Node', 'BypassAllLinksOnDelete'],
|
||||
name: 'Keep all links when deleting nodes',
|
||||
tooltip:
|
||||
'When deleting a node, attempt to reconnect all of its input and output links (bypassing the deleted node)',
|
||||
@@ -488,6 +510,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Node.MiddleClickRerouteNode',
|
||||
category: ['LiteGraph', 'Node', 'MiddleClickRerouteNode'],
|
||||
name: 'Middle-click creates a new Reroute node',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
@@ -495,6 +518,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.RerouteBeta',
|
||||
category: ['LiteGraph', 'RerouteBeta'],
|
||||
name: 'Opt-in to the reroute beta test',
|
||||
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.',
|
||||
@@ -505,6 +529,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.LinkMarkers',
|
||||
category: ['LiteGraph', 'Link', 'LinkMarkers'],
|
||||
name: 'Link midpoint markers',
|
||||
defaultValue: LinkMarkerShape.Circle,
|
||||
type: 'combo',
|
||||
@@ -517,9 +542,105 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.DOMClippingEnabled',
|
||||
category: ['Comfy', 'Node', 'DOMClippingEnabled'],
|
||||
category: ['LiteGraph', 'Node', 'DOMClippingEnabled'],
|
||||
name: 'Enable DOM element clipping (enabling may reduce performance)',
|
||||
type: 'boolean',
|
||||
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'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Server.ServerConfigValues',
|
||||
name: 'Server config values for frontend display',
|
||||
tooltip: 'Server config values used for frontend display only',
|
||||
type: 'hidden',
|
||||
// Mapping from server config id to value.
|
||||
defaultValue: {} as Record<string, any>,
|
||||
versionAdded: '1.4.8'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Server.LaunchArgs',
|
||||
name: 'Server launch arguments',
|
||||
tooltip:
|
||||
'These are the actual arguments that are passed to the server when it is launched.',
|
||||
type: 'hidden',
|
||||
defaultValue: {} as Record<string, string>,
|
||||
versionAdded: '1.4.8'
|
||||
}
|
||||
]
|
||||
432
src/constants/serverConfig.ts
Normal file
@@ -0,0 +1,432 @@
|
||||
import { FormItem } from '@/types/settingTypes'
|
||||
import {
|
||||
LatentPreviewMethod,
|
||||
LogLevel,
|
||||
HashFunction,
|
||||
AutoLaunch,
|
||||
CudaMalloc,
|
||||
FloatingPointPrecision,
|
||||
CrossAttentionMethod,
|
||||
VramManagement
|
||||
} from '@/types/serverArgs'
|
||||
|
||||
export interface ServerConfig<T> extends FormItem {
|
||||
id: string
|
||||
defaultValue: T
|
||||
category?: string[]
|
||||
// Override the default value getter with a custom function.
|
||||
getValue?: (value: T) => Record<string, any>
|
||||
}
|
||||
|
||||
export const WEB_ONLY_CONFIG_ITEMS: ServerConfig<any>[] = [
|
||||
// We only need these settings in the web version. Desktop app manages them already.
|
||||
{
|
||||
id: 'listen',
|
||||
name: 'Host: The IP address to listen on',
|
||||
category: ['Network'],
|
||||
type: 'text',
|
||||
defaultValue: '127.0.0.1'
|
||||
},
|
||||
{
|
||||
id: 'port',
|
||||
name: 'Port: The port to listen on',
|
||||
category: ['Network'],
|
||||
type: 'number',
|
||||
defaultValue: 8188
|
||||
}
|
||||
]
|
||||
|
||||
export const SERVER_CONFIG_ITEMS: ServerConfig<any>[] = [
|
||||
// Network settings
|
||||
{
|
||||
id: 'tls-keyfile',
|
||||
name: 'TLS Key File: Path to TLS key file for HTTPS',
|
||||
category: ['Network'],
|
||||
type: 'text',
|
||||
defaultValue: undefined
|
||||
},
|
||||
{
|
||||
id: 'tls-certfile',
|
||||
name: 'TLS Certificate File: Path to TLS certificate file for HTTPS',
|
||||
category: ['Network'],
|
||||
type: 'text',
|
||||
defaultValue: undefined
|
||||
},
|
||||
{
|
||||
id: 'enable-cors-header',
|
||||
name: 'Enable CORS header: Use "*" for all origins or specify domain',
|
||||
category: ['Network'],
|
||||
type: 'text',
|
||||
defaultValue: undefined
|
||||
},
|
||||
{
|
||||
id: 'max-upload-size',
|
||||
name: 'Maximum upload size (MB)',
|
||||
category: ['Network'],
|
||||
type: 'number',
|
||||
defaultValue: 100
|
||||
},
|
||||
|
||||
// Launch behavior
|
||||
{
|
||||
id: 'auto-launch',
|
||||
name: 'Automatically opens in the browser on startup',
|
||||
category: ['Launch'],
|
||||
type: 'combo',
|
||||
options: Object.values(AutoLaunch),
|
||||
defaultValue: AutoLaunch.Auto,
|
||||
getValue: (value: AutoLaunch) => {
|
||||
switch (value) {
|
||||
case AutoLaunch.Auto:
|
||||
return {}
|
||||
case AutoLaunch.Enable:
|
||||
return {
|
||||
['auto-launch']: true
|
||||
}
|
||||
case AutoLaunch.Disable:
|
||||
return {
|
||||
['disable-auto-launch']: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// CUDA settings
|
||||
{
|
||||
id: 'cuda-device',
|
||||
name: 'CUDA device index to use',
|
||||
category: ['CUDA'],
|
||||
type: 'number',
|
||||
defaultValue: undefined
|
||||
},
|
||||
{
|
||||
id: 'cuda-malloc',
|
||||
name: 'Use CUDA malloc for memory allocation',
|
||||
category: ['CUDA'],
|
||||
type: 'combo',
|
||||
options: Object.values(CudaMalloc),
|
||||
defaultValue: CudaMalloc.Auto,
|
||||
getValue: (value: CudaMalloc) => {
|
||||
switch (value) {
|
||||
case CudaMalloc.Auto:
|
||||
return {}
|
||||
case CudaMalloc.Enable:
|
||||
return {
|
||||
['cuda-malloc']: true
|
||||
}
|
||||
case CudaMalloc.Disable:
|
||||
return {
|
||||
['disable-cuda-malloc']: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Precision settings
|
||||
{
|
||||
id: 'global-precision',
|
||||
name: 'Global floating point precision',
|
||||
category: ['Inference'],
|
||||
type: 'combo',
|
||||
options: [
|
||||
FloatingPointPrecision.AUTO,
|
||||
FloatingPointPrecision.FP32,
|
||||
FloatingPointPrecision.FP16
|
||||
],
|
||||
defaultValue: FloatingPointPrecision.AUTO,
|
||||
tooltip: 'Global floating point precision',
|
||||
getValue: (value: FloatingPointPrecision) => {
|
||||
switch (value) {
|
||||
case FloatingPointPrecision.AUTO:
|
||||
return {}
|
||||
case FloatingPointPrecision.FP32:
|
||||
return {
|
||||
['force-fp32']: true
|
||||
}
|
||||
case FloatingPointPrecision.FP16:
|
||||
return {
|
||||
['force-fp16']: true
|
||||
}
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// UNET precision
|
||||
{
|
||||
id: 'unet-precision',
|
||||
name: 'UNET precision',
|
||||
category: ['Inference'],
|
||||
type: 'combo',
|
||||
options: [
|
||||
FloatingPointPrecision.AUTO,
|
||||
FloatingPointPrecision.FP16,
|
||||
FloatingPointPrecision.BF16,
|
||||
FloatingPointPrecision.FP8E4M3FN,
|
||||
FloatingPointPrecision.FP8E5M2
|
||||
],
|
||||
defaultValue: FloatingPointPrecision.AUTO,
|
||||
tooltip: 'UNET precision',
|
||||
getValue: (value: FloatingPointPrecision) => {
|
||||
switch (value) {
|
||||
case FloatingPointPrecision.AUTO:
|
||||
return {}
|
||||
default:
|
||||
return {
|
||||
[`${value.toLowerCase()}-unet`]: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// VAE settings
|
||||
{
|
||||
id: 'vae-precision',
|
||||
name: 'VAE precision',
|
||||
category: ['Inference'],
|
||||
type: 'combo',
|
||||
options: [
|
||||
FloatingPointPrecision.AUTO,
|
||||
FloatingPointPrecision.FP16,
|
||||
FloatingPointPrecision.FP32,
|
||||
FloatingPointPrecision.BF16
|
||||
],
|
||||
defaultValue: FloatingPointPrecision.AUTO,
|
||||
tooltip: 'VAE precision',
|
||||
getValue: (value: FloatingPointPrecision) => {
|
||||
switch (value) {
|
||||
case FloatingPointPrecision.AUTO:
|
||||
return {}
|
||||
default:
|
||||
return {
|
||||
[`${value.toLowerCase()}-vae`]: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'cpu-vae',
|
||||
name: 'Run VAE on CPU',
|
||||
category: ['Inference'],
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
|
||||
// Text Encoder settings
|
||||
{
|
||||
id: 'text-encoder-precision',
|
||||
name: 'Text Encoder precision',
|
||||
category: ['Inference'],
|
||||
type: 'combo',
|
||||
options: [
|
||||
FloatingPointPrecision.AUTO,
|
||||
FloatingPointPrecision.FP8E4M3FN,
|
||||
FloatingPointPrecision.FP8E5M2,
|
||||
FloatingPointPrecision.FP16,
|
||||
FloatingPointPrecision.FP32
|
||||
],
|
||||
defaultValue: FloatingPointPrecision.AUTO,
|
||||
tooltip: 'Text Encoder precision',
|
||||
getValue: (value: FloatingPointPrecision) => {
|
||||
switch (value) {
|
||||
case FloatingPointPrecision.AUTO:
|
||||
return {}
|
||||
default:
|
||||
return {
|
||||
[`${value.toLowerCase()}-text-enc`]: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Memory and performance settings
|
||||
{
|
||||
id: 'force-channels-last',
|
||||
name: 'Force channels-last memory format',
|
||||
category: ['Memory'],
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'directml',
|
||||
name: 'DirectML device index',
|
||||
category: ['Memory'],
|
||||
type: 'number',
|
||||
defaultValue: undefined
|
||||
},
|
||||
{
|
||||
id: 'disable-ipex-optimize',
|
||||
name: 'Disable IPEX optimization',
|
||||
category: ['Memory'],
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
|
||||
// Preview settings
|
||||
{
|
||||
id: 'preview-method',
|
||||
name: 'Method used for latent previews',
|
||||
category: ['Preview'],
|
||||
type: 'combo',
|
||||
options: Object.values(LatentPreviewMethod),
|
||||
defaultValue: LatentPreviewMethod.NoPreviews
|
||||
},
|
||||
{
|
||||
id: 'preview-size',
|
||||
name: 'Size of preview images',
|
||||
category: ['Preview'],
|
||||
type: 'slider',
|
||||
defaultValue: 512,
|
||||
attrs: {
|
||||
min: 128,
|
||||
max: 2048,
|
||||
step: 128
|
||||
}
|
||||
},
|
||||
|
||||
// Cache settings
|
||||
{
|
||||
id: 'cache-classic',
|
||||
name: 'Use classic cache system',
|
||||
category: ['Cache'],
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'cache-lru',
|
||||
name: 'Use LRU caching with a maximum of N node results cached. (0 to disable).',
|
||||
category: ['Cache'],
|
||||
type: 'number',
|
||||
defaultValue: 0,
|
||||
tooltip: 'May use more RAM/VRAM.'
|
||||
},
|
||||
|
||||
// Attention settings
|
||||
{
|
||||
id: 'cross-attention-method',
|
||||
name: 'Cross attention method',
|
||||
category: ['Attention'],
|
||||
type: 'combo',
|
||||
options: Object.values(CrossAttentionMethod),
|
||||
defaultValue: CrossAttentionMethod.Auto,
|
||||
getValue: (value: CrossAttentionMethod) => {
|
||||
switch (value) {
|
||||
case CrossAttentionMethod.Auto:
|
||||
return {}
|
||||
default:
|
||||
return {
|
||||
[`use-${value.toLowerCase()}-cross-attention`]: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'disable-xformers',
|
||||
name: 'Disable xFormers optimization',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'force-upcast-attention',
|
||||
name: 'Force attention upcast',
|
||||
category: ['Attention'],
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'dont-upcast-attention',
|
||||
name: 'Prevent attention upcast',
|
||||
category: ['Attention'],
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
|
||||
// VRAM management
|
||||
{
|
||||
id: 'vram-management',
|
||||
name: 'VRAM management mode',
|
||||
category: ['Memory'],
|
||||
type: 'combo',
|
||||
options: Object.values(VramManagement),
|
||||
defaultValue: VramManagement.Auto,
|
||||
getValue: (value: VramManagement) => {
|
||||
switch (value) {
|
||||
case VramManagement.Auto:
|
||||
return {}
|
||||
default:
|
||||
return {
|
||||
[value]: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'reserve-vram',
|
||||
name: 'Reserved VRAM (GB)',
|
||||
category: ['Memory'],
|
||||
type: 'number',
|
||||
defaultValue: undefined,
|
||||
tooltip:
|
||||
'Set the amount of vram in GB you want to reserve for use by your OS/other software. By default some amount is reverved depending on your OS.'
|
||||
},
|
||||
|
||||
// Misc settings
|
||||
{
|
||||
id: 'default-hashing-function',
|
||||
name: 'Default hashing function for model files',
|
||||
type: 'combo',
|
||||
options: Object.values(HashFunction),
|
||||
defaultValue: HashFunction.SHA256
|
||||
},
|
||||
{
|
||||
id: 'disable-smart-memory',
|
||||
name: 'Force ComfyUI to agressively offload to regular ram instead of keeping models in vram when it can.',
|
||||
category: ['Memory'],
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'deterministic',
|
||||
name: 'Make pytorch use slower deterministic algorithms when it can.',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
tooltip: 'Note that this might not make images deterministic in all cases.'
|
||||
},
|
||||
{
|
||||
id: 'fast',
|
||||
name: 'Enable some untested and potentially quality deteriorating optimizations.',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'dont-print-server',
|
||||
name: "Don't print server output to console.",
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'disable-metadata',
|
||||
name: 'Disable saving prompt metadata in files.',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'disable-all-custom-nodes',
|
||||
name: 'Disable loading all custom nodes.',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'log-level',
|
||||
name: 'Logging verbosity level',
|
||||
type: 'combo',
|
||||
options: Object.values(LogLevel),
|
||||
defaultValue: LogLevel.INFO,
|
||||
getValue: (value: LogLevel) => {
|
||||
return {
|
||||
verbose: value
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -733,7 +733,7 @@ app.registerExtension({
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id,
|
||||
category: ['Comfy', 'ColorPalette'],
|
||||
category: ['Appearance', 'ColorPalette'],
|
||||
name: 'Color Palette',
|
||||
type: (name, setter, value) => {
|
||||
const options = [
|
||||
|
||||
147
src/extensions/core/electronAdapter.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
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()
|
||||
|
||||
const onChangeRestartApp = (newValue: string, oldValue: string) => {
|
||||
// Add a delay to allow changes to take effect before restarting.
|
||||
if (oldValue !== undefined && newValue !== oldValue) {
|
||||
electronAPI.restartApp('Restart ComfyUI to apply changes.', 1500)
|
||||
}
|
||||
}
|
||||
|
||||
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: onChangeRestartApp
|
||||
},
|
||||
{
|
||||
id: 'Comfy-Desktop.SendStatistics',
|
||||
category: ['Comfy-Desktop', 'General', 'Send Statistics'],
|
||||
name: 'Send anonymous crash reports',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
onChange: onChangeRestartApp
|
||||
}
|
||||
],
|
||||
|
||||
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'
|
||||
}
|
||||
]
|
||||
})
|
||||
})()
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
deserialiseAndCreate,
|
||||
serialise
|
||||
} from '@/extensions/core/vintageClipboard'
|
||||
import type { ComfyNodeDef } from '@/types/apiTypes'
|
||||
|
||||
type GroupNodeWorkflowData = {
|
||||
external: ComfyLink[]
|
||||
@@ -56,7 +57,7 @@ const Workflow = {
|
||||
|
||||
class GroupNodeBuilder {
|
||||
nodes: LGraphNode[]
|
||||
nodeData: any
|
||||
nodeData: GroupNodeWorkflowData
|
||||
|
||||
constructor(nodes: LGraphNode[]) {
|
||||
this.nodes = nodes
|
||||
@@ -175,7 +176,7 @@ export class GroupNodeConfig {
|
||||
primitiveToWidget: {}
|
||||
nodeInputs: {}
|
||||
outputVisibility: any[]
|
||||
nodeDef: any
|
||||
nodeDef: ComfyNodeDef
|
||||
inputs: any[]
|
||||
linksFrom: {}
|
||||
linksTo: {}
|
||||
@@ -204,6 +205,7 @@ export class GroupNodeConfig {
|
||||
output: [],
|
||||
output_name: [],
|
||||
output_is_list: [],
|
||||
// @ts-expect-error Unused, doesn't exist
|
||||
output_is_hidden: [],
|
||||
name: source + SEPARATOR + this.name,
|
||||
display_name: this.name,
|
||||
@@ -695,11 +697,11 @@ export class GroupNodeConfig {
|
||||
}
|
||||
|
||||
export class GroupNodeHandler {
|
||||
node
|
||||
node: LGraphNode
|
||||
groupData
|
||||
innerNodes: any
|
||||
|
||||
constructor(node) {
|
||||
constructor(node: LGraphNode) {
|
||||
this.node = node
|
||||
this.groupData = node.constructor?.nodeData?.[GROUP]
|
||||
|
||||
@@ -774,6 +776,7 @@ export class GroupNodeHandler {
|
||||
|
||||
this.node.updateLink = (link) => {
|
||||
// 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 }
|
||||
const output = this.groupData.newToOldOutputMap[link.origin_slot]
|
||||
let innerNode = this.innerNodes[output.node.index]
|
||||
@@ -965,17 +968,20 @@ export class GroupNodeHandler {
|
||||
|
||||
app.canvas.emitBeforeChange()
|
||||
|
||||
const { newNodes, selectedIds } = addInnerNodes()
|
||||
reconnectInputs(selectedIds)
|
||||
reconnectOutputs(selectedIds)
|
||||
app.graph.remove(this.node)
|
||||
try {
|
||||
const { newNodes, selectedIds } = addInnerNodes()
|
||||
reconnectInputs(selectedIds)
|
||||
reconnectOutputs(selectedIds)
|
||||
app.graph.remove(this.node)
|
||||
|
||||
app.canvas.emitAfterChange()
|
||||
|
||||
return newNodes
|
||||
return newNodes
|
||||
} finally {
|
||||
app.canvas.emitAfterChange()
|
||||
}
|
||||
}
|
||||
|
||||
const getExtraMenuOptions = this.node.getExtraMenuOptions
|
||||
// @ts-expect-error Should pass patched return value getExtraMenuOptions
|
||||
this.node.getExtraMenuOptions = function (_, options) {
|
||||
getExtraMenuOptions?.apply(this, arguments)
|
||||
|
||||
@@ -988,6 +994,7 @@ export class GroupNodeHandler {
|
||||
null,
|
||||
{
|
||||
content: 'Convert to nodes',
|
||||
// @ts-expect-error
|
||||
callback: () => {
|
||||
return this.convertToNodes()
|
||||
}
|
||||
@@ -1148,6 +1155,7 @@ export class GroupNodeHandler {
|
||||
|
||||
if (
|
||||
old.inputName !== 'image' &&
|
||||
// @ts-expect-error Widget values
|
||||
!widget.options.values.includes(widget.value)
|
||||
) {
|
||||
widget.value = widget.options.values[0]
|
||||
@@ -1354,6 +1362,7 @@ export class GroupNodeHandler {
|
||||
if (!originNode) continue // this node is in the group
|
||||
originNode.connect(
|
||||
originSlot,
|
||||
// @ts-expect-error Valid - uses deprecated interface. Required check: if (graph.getNodeById(this.node.id) !== this.node) report()
|
||||
this.node.id,
|
||||
this.groupData.oldToNewInputMap[targetId][targetSlot]
|
||||
)
|
||||
@@ -1475,7 +1484,7 @@ function ungroupSelectedGroupNodes() {
|
||||
const nodes = Object.values(app.canvas.selected_nodes ?? {})
|
||||
for (const node of nodes) {
|
||||
if (GroupNodeHandler.isGroupNode(node)) {
|
||||
node['convertToNodes']?.()
|
||||
node.convertToNodes?.()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { app } from '../../scripts/app'
|
||||
import { LGraphCanvas } from '@comfyorg/litegraph'
|
||||
import type { Positionable } from '@comfyorg/litegraph/dist/interfaces'
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
function setNodeMode(node: LGraphNode, mode: number) {
|
||||
node.mode = mode
|
||||
@@ -11,7 +12,8 @@ function setNodeMode(node: LGraphNode, mode: number) {
|
||||
}
|
||||
|
||||
function addNodesToGroup(group: LGraphGroup, items: Iterable<Positionable>) {
|
||||
group.resizeTo([...group.children, ...items])
|
||||
const padding = useSettingStore().get('Comfy.GroupSelectedNodes.Padding')
|
||||
group.resizeTo([...group.children, ...items], padding)
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
@@ -76,7 +78,10 @@ app.registerExtension({
|
||||
content: 'Fit Group To Nodes',
|
||||
callback: () => {
|
||||
group.recomputeInsideNodes()
|
||||
group.resizeTo(group.children)
|
||||
const padding = useSettingStore().get(
|
||||
'Comfy.GroupSelectedNodes.Padding'
|
||||
)
|
||||
group.resizeTo(group.children, padding)
|
||||
this.graph.change()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -15,8 +15,9 @@ import './rerouteNode'
|
||||
import './saveImageExtraOutput'
|
||||
import './simpleTouchSupport'
|
||||
import './slotDefaults'
|
||||
import './snapToGrid'
|
||||
import './uploadImage'
|
||||
import './webcamCapture'
|
||||
import './widgetInputs'
|
||||
import './uploadAudio'
|
||||
import './electronAdapter'
|
||||
import './load3d'
|
||||
|
||||
@@ -24,7 +24,7 @@ app.registerExtension({
|
||||
}
|
||||
app.ui.settings.addSetting({
|
||||
id,
|
||||
category: ['Comfy', 'Graph', 'InvertMenuScrolling'],
|
||||
category: ['LiteGraph', 'Menu', 'InvertMenuScrolling'],
|
||||
name: 'Invert Context Menu Scrolling',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
|
||||
1641
src/extensions/core/load3d.ts
Normal file
1146
src/extensions/core/maskEditorOld.ts
Normal file
@@ -44,7 +44,7 @@ app.registerExtension({
|
||||
lastTouch = getMultiTouchCenter(e)
|
||||
|
||||
touchDist = getMultiTouchPos(e)
|
||||
app.canvas.pointer_is_down = false
|
||||
app.canvas.pointer.isDown = false
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -78,7 +78,7 @@ app.registerExtension({
|
||||
touchTime = null
|
||||
if (e.touches?.length === 2 && lastTouch && !e.ctrlKey && !e.shiftKey) {
|
||||
e.preventDefault() // Prevent browser from zooming when two textareas are touched
|
||||
app.canvas.pointer_is_down = false
|
||||
app.canvas.pointer.isDown = false
|
||||
touchZooming = true
|
||||
|
||||
LiteGraph.closeAllContextMenus(window)
|
||||
@@ -137,7 +137,7 @@ LGraphCanvas.prototype.processMouseDown = function (e) {
|
||||
if (touchZooming || touchCount) {
|
||||
return
|
||||
}
|
||||
app.canvas.pointer_is_down = false // Prevent context menu from opening on second tap
|
||||
app.canvas.pointer.isDown = false // Prevent context menu from opening on second tap
|
||||
return processMouseDown.apply(this, arguments)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
// @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
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -69,51 +69,54 @@ export function deserialiseAndCreate(data: string, canvas: LGraphCanvas): void {
|
||||
|
||||
const { graph, graph_mouse } = canvas
|
||||
canvas.emitBeforeChange()
|
||||
graph.beforeChange()
|
||||
try {
|
||||
graph.beforeChange()
|
||||
|
||||
const deserialised = JSON.parse(data)
|
||||
const deserialised = JSON.parse(data)
|
||||
|
||||
// Find the top left point of the boundary of all pasted nodes
|
||||
const topLeft = [Infinity, Infinity]
|
||||
for (const { pos } of deserialised.nodes) {
|
||||
if (topLeft[0] > pos[0]) topLeft[0] = pos[0]
|
||||
if (topLeft[1] > pos[1]) topLeft[1] = pos[1]
|
||||
// Find the top left point of the boundary of all pasted nodes
|
||||
const topLeft = [Infinity, Infinity]
|
||||
for (const { pos } of deserialised.nodes) {
|
||||
if (topLeft[0] > pos[0]) topLeft[0] = pos[0]
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -894,7 +894,7 @@ app.registerExtension({
|
||||
// Not a widget input or already handled input
|
||||
if (
|
||||
!(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)
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
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'
|
||||
}
|
||||
}
|
||||
25
src/hooks/bottomPanelTabs/terminalTabs.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
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'
|
||||
}
|
||||
}
|
||||
71
src/hooks/bottomPanelTabs/useTerminal.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import { debounce } from 'lodash'
|
||||
import { markRaw, onMounted, onUnmounted, Ref } from 'vue'
|
||||
import '@xterm/xterm/css/xterm.css'
|
||||
|
||||
export function useTerminal(element: Ref<HTMLElement>) {
|
||||
const fitAddon = new FitAddon()
|
||||
const terminal = markRaw(
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||