Compare commits
2 Commits
fix-queue-
...
node-templ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
782d93a7a0 | ||
|
|
7be14c5189 |
@@ -6,11 +6,6 @@ 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 run test:jest:fast -- --verbose
|
||||
npm test -- --verbose
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
playwright-tests-chromium:
|
||||
|
||||
@@ -431,8 +431,6 @@ 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:
|
||||
|
||||
```
|
||||
|
||||
10
browser_tests/assets/node_template_templates.json
Normal file
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"name": "Three Nodes Template",
|
||||
"data": "{\"nodes\":[{\"id\":7,\"type\":\"CLIPTextEncode\",\"pos\":[413,389],\"size\":[425.27801513671875,180.6060791015625],\"flags\":{},\"order\":3,\"mode\":0,\"inputs\":[{\"name\":\"clip\",\"type\":\"CLIP\",\"link\":null}],\"outputs\":[{\"name\":\"CONDITIONING\",\"type\":\"CONDITIONING\",\"links\":[],\"slot_index\":0}],\"properties\":{\"Node name for S&R\":\"CLIPTextEncode\"},\"widgets_values\":[\"text, watermark\"]},{\"id\":6,\"type\":\"CLIPTextEncode\",\"pos\":[415,186],\"size\":[422.84503173828125,164.31304931640625],\"flags\":{},\"order\":2,\"mode\":0,\"inputs\":[{\"name\":\"clip\",\"type\":\"CLIP\",\"link\":null}],\"outputs\":[{\"name\":\"CONDITIONING\",\"type\":\"CONDITIONING\",\"links\":[],\"slot_index\":0}],\"properties\":{\"Node name for S&R\":\"CLIPTextEncode\"},\"widgets_values\":[\"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,\"]},{\"id\":4,\"type\":\"CheckpointLoaderSimple\",\"pos\":[26,474],\"size\":[315,98],\"flags\":{},\"order\":1,\"mode\":0,\"inputs\":[],\"outputs\":[{\"name\":\"MODEL\",\"type\":\"MODEL\",\"links\":[],\"slot_index\":0},{\"name\":\"CLIP\",\"type\":\"CLIP\",\"links\":[],\"slot_index\":1},{\"name\":\"VAE\",\"type\":\"VAE\",\"links\":[],\"slot_index\":2}],\"properties\":{\"Node name for S&R\":\"CheckpointLoaderSimple\"},\"widgets_values\":[\"v1-5-pruned-emaonly.ckpt\"]}],\"groups\":[],\"reroutes\":[],\"links\":[{\"id\":5,\"origin_id\":4,\"origin_slot\":1,\"target_id\":7,\"target_slot\":0,\"type\":\"CLIP\"},{\"id\":3,\"origin_id\":4,\"origin_slot\":1,\"target_id\":6,\"target_slot\":0,\"type\":\"CLIP\"}]}"
|
||||
},
|
||||
{
|
||||
"name": "Completely empty template",
|
||||
"data": "{\"nodes\":[],\"groups\":[],\"reroutes\":[],\"links\":[]}"
|
||||
}
|
||||
]
|
||||
@@ -1,90 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
6
browser_tests/assets/vintage_clipboard_template.json
Normal file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"name": "vintageClipboard Template",
|
||||
"data": "{\"nodes\":[{\"id\":-1,\"type\":\"CheckpointLoaderSimple\",\"pos\":[26,474],\"size\":[315,98],\"flags\":{},\"order\":1,\"mode\":0,\"inputs\":[],\"outputs\":[{\"name\":\"MODEL\",\"type\":\"MODEL\",\"links\":[],\"slot_index\":0},{\"name\":\"CLIP\",\"type\":\"CLIP\",\"links\":[],\"slot_index\":1},{\"name\":\"VAE\",\"type\":\"VAE\",\"links\":[],\"slot_index\":2}],\"properties\":{\"Node name for S&R\":\"CheckpointLoaderSimple\"},\"widgets_values\":[\"v1-5-pruned-emaonly.ckpt\"]},{\"id\":-1,\"type\":\"CLIPTextEncode\",\"pos\":[415,186],\"size\":[422.84503173828125,164.31304931640625],\"flags\":{},\"order\":2,\"mode\":0,\"inputs\":[{\"name\":\"clip\",\"type\":\"CLIP\",\"link\":null}],\"outputs\":[{\"name\":\"CONDITIONING\",\"type\":\"CONDITIONING\",\"links\":[],\"slot_index\":0}],\"properties\":{\"Node name for S&R\":\"CLIPTextEncode\"},\"widgets_values\":[\"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,\"]},{\"id\":-1,\"type\":\"CLIPTextEncode\",\"pos\":[413,389],\"size\":[425.27801513671875,180.6060791015625],\"flags\":{},\"order\":3,\"mode\":0,\"inputs\":[{\"name\":\"clip\",\"type\":\"CLIP\",\"link\":null}],\"outputs\":[{\"name\":\"CONDITIONING\",\"type\":\"CONDITIONING\",\"links\":[],\"slot_index\":0}],\"properties\":{\"Node name for S&R\":\"CLIPTextEncode\"},\"widgets_values\":[\"text, watermark\"]}],\"links\":[[0,1,1,0,4],[0,1,2,0,4]]}"
|
||||
}
|
||||
]
|
||||
@@ -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,33 +56,34 @@ test.describe('Change Tracker', () => {
|
||||
expect(await comfyPage.getToastErrorCount()).toBe(0)
|
||||
expect(await isModified()).toBe(false)
|
||||
|
||||
expect(await getUndoQueueSize()).toBe(0)
|
||||
// TODO(huchenlei): Investigate why saving the workflow is causing the
|
||||
// undo queue to be triggered.
|
||||
expect(await getUndoQueueSize()).toBe(1)
|
||||
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(1)
|
||||
expect(await getUndoQueueSize()).toBe(2)
|
||||
expect(await getRedoQueueSize()).toBe(0)
|
||||
|
||||
await comfyPage.ctrlB()
|
||||
await expect(node).toBeBypassed()
|
||||
expect(await isModified()).toBe(true)
|
||||
expect(await getUndoQueueSize()).toBe(2)
|
||||
expect(await getUndoQueueSize()).toBe(3)
|
||||
expect(await getRedoQueueSize()).toBe(0)
|
||||
|
||||
await comfyPage.ctrlZ()
|
||||
await expect(node).not.toBeBypassed()
|
||||
expect(await isModified()).toBe(true)
|
||||
expect(await getUndoQueueSize()).toBe(1)
|
||||
expect(await getUndoQueueSize()).toBe(2)
|
||||
expect(await getRedoQueueSize()).toBe(1)
|
||||
|
||||
await comfyPage.ctrlZ()
|
||||
await expect(node).not.toBeCollapsed()
|
||||
expect(await isModified()).toBe(false)
|
||||
expect(await getUndoQueueSize()).toBe(0)
|
||||
expect(await getUndoQueueSize()).toBe(1)
|
||||
expect(await getRedoQueueSize()).toBe(2)
|
||||
})
|
||||
})
|
||||
@@ -97,7 +98,6 @@ 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,10 +111,6 @@ 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)
|
||||
|
||||
@@ -156,7 +152,6 @@ 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: 138 KiB After Width: | Height: | Size: 139 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: 157 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 152 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.
|
||||
// KSampler's seed
|
||||
await comfyPage.canvas.click({
|
||||
position: {
|
||||
x: 1005,
|
||||
y: 281
|
||||
}
|
||||
})
|
||||
await comfyPage.ctrlC(null)
|
||||
// Empty latent node's width
|
||||
await comfyPage.canvas.click({
|
||||
position: {
|
||||
@@ -39,6 +31,14 @@ test.describe('Copy Paste', () => {
|
||||
y: 643
|
||||
}
|
||||
})
|
||||
await comfyPage.ctrlC(null)
|
||||
// KSampler's seed
|
||||
await comfyPage.canvas.click({
|
||||
position: {
|
||||
x: 1005,
|
||||
y: 281
|
||||
}
|
||||
})
|
||||
await comfyPage.ctrlV(null)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('copied-widget-value.png')
|
||||
|
||||
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 96 KiB |
@@ -94,7 +94,7 @@ test.describe('Settings', () => {
|
||||
test('Can change canvas zoom speed setting', async ({ comfyPage }) => {
|
||||
const maxSpeed = 2.5
|
||||
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed)
|
||||
test.step('Setting should persist', async () => {
|
||||
await test.step('Setting should persist', async () => {
|
||||
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -77,6 +77,7 @@ export class ComfyPage {
|
||||
// All canvas position operations are based on default view of canvas.
|
||||
public readonly canvas: Locator
|
||||
public readonly widgetTextBox: Locator
|
||||
public readonly contextMenu: Locator
|
||||
|
||||
// Buttons
|
||||
public readonly resetViewButton: Locator
|
||||
@@ -107,6 +108,7 @@ export class ComfyPage {
|
||||
this.url = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'
|
||||
this.canvas = page.locator('#graph-canvas')
|
||||
this.widgetTextBox = page.getByPlaceholder('text').nth(1)
|
||||
this.contextMenu = page.locator('.litegraph.litecontextmenu')
|
||||
this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
|
||||
this.queueButton = page.getByRole('button', { name: 'Queue Prompt' })
|
||||
this.workflowUploadInput = page.locator('#comfy-file-input')
|
||||
@@ -148,6 +150,12 @@ export class ComfyPage {
|
||||
})
|
||||
}
|
||||
|
||||
async getGraphSelectedItemsCount(): Promise<number | undefined> {
|
||||
return await this.page.evaluate(() => {
|
||||
return window['app']?.canvas?.selectedItems?.size
|
||||
})
|
||||
}
|
||||
|
||||
async setupWorkflowsDirectory(structure: FolderStructure) {
|
||||
const resp = await this.request.post(
|
||||
`${this.url}/api/devtools/setup_folder_structure`,
|
||||
@@ -191,6 +199,39 @@ export class ComfyPage {
|
||||
return await resp.json()
|
||||
}
|
||||
|
||||
async clearNodeTemplates() {
|
||||
const resp = await this.request.delete(
|
||||
`${this.url}/api/userdata/comfy.templates.json`,
|
||||
{
|
||||
headers: { 'Comfy-User': this.id }
|
||||
}
|
||||
)
|
||||
|
||||
const status = resp.status()
|
||||
if (status !== 204 && status !== 404)
|
||||
throw new Error(`Failed to delete node templates: ${await resp.text()}`)
|
||||
}
|
||||
|
||||
async setNodeTemplates(fileName: string) {
|
||||
const path = this.assetPath(fileName)
|
||||
const data = fs.readFileSync(path, 'utf-8')
|
||||
|
||||
const resp = await this.request.post(
|
||||
`${this.url}/api/userdata/comfy.templates.json`,
|
||||
{
|
||||
headers: {
|
||||
'Comfy-User': this.id,
|
||||
overwrite: 'true',
|
||||
full_info: 'true'
|
||||
},
|
||||
data
|
||||
}
|
||||
)
|
||||
|
||||
if (resp.status() !== 200)
|
||||
throw new Error(`Failed to upload node templates: ${await resp.text()}`)
|
||||
}
|
||||
|
||||
async setupSettings(settings: Record<string, any>) {
|
||||
const resp = await this.request.post(
|
||||
`${this.url}/api/devtools/set_settings`,
|
||||
@@ -204,15 +245,13 @@ export class ComfyPage {
|
||||
}
|
||||
}
|
||||
|
||||
async setup({ clearStorage = true }: { clearStorage?: boolean } = {}) {
|
||||
async setup() {
|
||||
await this.goto()
|
||||
if (clearStorage) {
|
||||
await this.page.evaluate((id) => {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
localStorage.setItem('Comfy.userId', id)
|
||||
}, this.id)
|
||||
}
|
||||
await this.page.evaluate((id) => {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
localStorage.setItem('Comfy.userId', id)
|
||||
}, this.id)
|
||||
await this.goto()
|
||||
|
||||
// Unify font for consistent screenshots.
|
||||
@@ -316,9 +355,9 @@ export class ComfyPage {
|
||||
}, settingId)
|
||||
}
|
||||
|
||||
async reload({ clearStorage = true }: { clearStorage?: boolean } = {}) {
|
||||
async reload() {
|
||||
await this.page.reload({ timeout: 15000 })
|
||||
await this.setup({ clearStorage })
|
||||
await this.setup()
|
||||
}
|
||||
|
||||
async goto() {
|
||||
@@ -401,11 +440,17 @@ export class ComfyPage {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async dragAndDrop(source: Position, target: Position) {
|
||||
async dragAndDrop(
|
||||
source: Position,
|
||||
target: Position,
|
||||
modifierKey?: 'ControlOrMeta' | 'Control' | 'Alt' | 'Shift'
|
||||
) {
|
||||
if (modifierKey) await this.page.keyboard.down(modifierKey)
|
||||
await this.page.mouse.move(source.x, source.y)
|
||||
await this.page.mouse.down()
|
||||
await this.page.mouse.move(target.x, target.y)
|
||||
await this.page.mouse.up()
|
||||
if (modifierKey) await this.page.keyboard.up(modifierKey)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
@@ -526,6 +571,9 @@ 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()
|
||||
@@ -550,13 +598,11 @@ export class ComfyPage {
|
||||
}
|
||||
|
||||
async rightClickCanvas() {
|
||||
await this.page.mouse.click(10, 10, { button: 'right' })
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async clickContextMenuItem(name: string): Promise<void> {
|
||||
await this.page.getByRole('menuitem', { name }).click()
|
||||
await this.nextFrame()
|
||||
await this.canvas.click({
|
||||
position: { x: 10, y: 10 },
|
||||
button: 'right'
|
||||
})
|
||||
await expect(this.contextMenu).toBeVisible()
|
||||
}
|
||||
|
||||
async doubleClickCanvas() {
|
||||
@@ -571,7 +617,7 @@ export class ComfyPage {
|
||||
y: 625
|
||||
}
|
||||
})
|
||||
this.page.mouse.move(10, 10)
|
||||
await this.page.mouse.move(10, 10)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
@@ -583,10 +629,14 @@ export class ComfyPage {
|
||||
},
|
||||
button: 'right'
|
||||
})
|
||||
this.page.mouse.move(10, 10)
|
||||
await this.page.mouse.move(10, 10)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async clickContextMenuItem(name: string): Promise<void> {
|
||||
await this.page.getByRole('menuitem', { name }).click()
|
||||
}
|
||||
|
||||
async select2Nodes() {
|
||||
// Select 2 CLIP nodes.
|
||||
await this.page.keyboard.down('Control')
|
||||
@@ -737,19 +787,6 @@ export class ComfyPage {
|
||||
)
|
||||
}
|
||||
|
||||
async confirmDialog(prompt: string, text: string = 'Yes') {
|
||||
const modal = this.page.locator(
|
||||
`.comfy-modal-content:has-text("${prompt}")`
|
||||
)
|
||||
await expect(modal).toBeVisible()
|
||||
await modal
|
||||
.locator('.comfyui-button', {
|
||||
hasText: text
|
||||
})
|
||||
.click()
|
||||
await expect(modal).toBeHidden()
|
||||
}
|
||||
|
||||
async convertAllNodesToGroupNode(groupNodeName: string) {
|
||||
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,12 +103,6 @@ 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,13 +77,8 @@ test.describe('Group Node', () => {
|
||||
.click()
|
||||
})
|
||||
})
|
||||
// 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 }) => {
|
||||
|
||||
test('Can be added to canvas using search', async ({ comfyPage }) => {
|
||||
const groupNodeName = 'DefautWorkflowGroupNode'
|
||||
await comfyPage.convertAllNodesToGroupNode(groupNodeName)
|
||||
await comfyPage.doubleClickCanvas()
|
||||
|
||||
@@ -320,15 +320,6 @@ 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 ({
|
||||
@@ -546,34 +537,6 @@ 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: 100 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 101 KiB |
@@ -379,9 +379,7 @@ test.describe('Menu', () => {
|
||||
// Open the sidebar
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
await tab.open()
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setupWorkflowsDirectory({})
|
||||
})
|
||||
|
||||
@@ -394,7 +392,7 @@ test.describe('Menu', () => {
|
||||
await tab.newBlankWorkflowButton.click()
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json',
|
||||
'*Unsaved Workflow (2).json'
|
||||
'Unsaved Workflow (2).json'
|
||||
])
|
||||
})
|
||||
|
||||
@@ -452,44 +450,6 @@ 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
|
||||
}) => {
|
||||
@@ -515,12 +475,12 @@ test.describe('Menu', () => {
|
||||
`tempWorkflow-${test.info().title}`
|
||||
)
|
||||
const closeButton = comfyPage.page.locator(
|
||||
'.comfyui-workflows-open .close-workflow-button'
|
||||
'.comfyui-workflows-open .p-button-icon.pi-times'
|
||||
)
|
||||
await closeButton.click()
|
||||
expect(
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
).toEqual(['*Unsaved Workflow.json'])
|
||||
).toEqual(['Unsaved Workflow.json'])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
70
browser_tests/nodeTemplate.spec.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
comfyPageFixture as test,
|
||||
comfyExpect as expect
|
||||
} from './fixtures/ComfyPage'
|
||||
|
||||
// Old `nodeTemplate.ts` system
|
||||
test.describe('Node Template', () => {
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.clearNodeTemplates()
|
||||
})
|
||||
|
||||
test('Can create and use node template', async ({ comfyPage }) => {
|
||||
const templateName = 'Can create node template template'
|
||||
|
||||
await comfyPage.clearNodeTemplates()
|
||||
await comfyPage.reload()
|
||||
|
||||
// TODO: Flaky test. Right click requires delay after reload, but other interactions do not.
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
|
||||
// Enter filename when prompt dialog shown
|
||||
comfyPage.page.on('dialog', (dialog) => dialog.accept(templateName))
|
||||
|
||||
// Ctrl + drag over 3 nodes
|
||||
await comfyPage.dragAndDrop(
|
||||
{ x: 175, y: 252 },
|
||||
{ x: 483, y: 564 },
|
||||
'ControlOrMeta'
|
||||
)
|
||||
expect(await comfyPage.getGraphSelectedItemsCount()).toEqual(3)
|
||||
|
||||
await comfyPage.rightClickCanvas()
|
||||
await comfyPage.clickContextMenuItem('Save Selected as Template')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.rightClickCanvas()
|
||||
await comfyPage.clickContextMenuItem('Node Templates >')
|
||||
await comfyPage.clickContextMenuItem(templateName)
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot()
|
||||
})
|
||||
|
||||
test('Can load old format template', async ({ comfyPage }) => {
|
||||
await comfyPage.setNodeTemplates('vintage_clipboard_template.json')
|
||||
await comfyPage.reload()
|
||||
|
||||
// TODO: Flaky test. Right click requires delay after reload, but other interactions do not.
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
|
||||
await comfyPage.rightClickCanvas()
|
||||
await comfyPage.clickContextMenuItem('Node Templates >')
|
||||
await comfyPage.clickContextMenuItem('vintageClipboard Template')
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot()
|
||||
})
|
||||
|
||||
test('Can load new format template', async ({ comfyPage }) => {
|
||||
await comfyPage.setNodeTemplates('node_template_templates.json')
|
||||
await comfyPage.reload()
|
||||
|
||||
// TODO: Flaky test. Right click requires delay after reload, but other interactions do not.
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
|
||||
await comfyPage.rightClickCanvas()
|
||||
await comfyPage.clickContextMenuItem('Node Templates >')
|
||||
await comfyPage.clickContextMenuItem('Three Nodes Template')
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot()
|
||||
})
|
||||
})
|
||||
@@ -36,7 +36,7 @@ test.describe('Canvas Right Click Menu', () => {
|
||||
await dialog.accept('GroupNode2CLIP')
|
||||
})
|
||||
await comfyPage.rightClickCanvas()
|
||||
await comfyPage.clickContextMenuItem('Convert to Group Node')
|
||||
await comfyPage.page.getByText('Convert to Group Node').click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'right-click-node-group-node.png'
|
||||
|
||||
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 98 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: 97 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 97 KiB |
@@ -14,7 +14,6 @@ const jestConfig: JestConfigWithTsJest = {
|
||||
}
|
||||
]
|
||||
},
|
||||
transformIgnorePatterns: ['/node_modules/(?!(three|@three)/)'],
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
'\\.(css|less|scss|sass)$': 'identity-obj-proxy'
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
export default {
|
||||
'./**/*.js': (stagedFiles) => formatFiles(stagedFiles),
|
||||
|
||||
'./**/*.{ts,tsx,vue}': (stagedFiles) => [
|
||||
...formatFiles(stagedFiles),
|
||||
'tsc --noEmit',
|
||||
'tsc-strict'
|
||||
]
|
||||
}
|
||||
|
||||
function formatFiles(fileNames) {
|
||||
return [`prettier --write ${fileNames.join(' ')}`]
|
||||
}
|
||||
119
package-lock.json
generated
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "comfyui-frontend",
|
||||
"version": "1.4.9",
|
||||
"version": "1.3.43",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "comfyui-frontend",
|
||||
"version": "1.4.9",
|
||||
"version": "1.3.43",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.3.6",
|
||||
"@comfyorg/litegraph": "^0.8.35",
|
||||
"@comfyorg/comfyui-electron-types": "^0.2.16",
|
||||
"@comfyorg/litegraph": "^0.8.26",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
@@ -24,7 +24,6 @@
|
||||
"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",
|
||||
@@ -41,7 +40,6 @@
|
||||
"@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",
|
||||
@@ -65,6 +63,7 @@
|
||||
"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",
|
||||
@@ -1917,15 +1916,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@comfyorg/comfyui-electron-types": {
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.3.6.tgz",
|
||||
"integrity": "sha512-wgMgESnCcRzvVkk8CwWiTAUJxC4LBvw5uTENxzaWkEL0qrnmiGrVLore00yX3cYz04hJaTA6PqasLqgVLDOenw==",
|
||||
"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==",
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.8.35",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.35.tgz",
|
||||
"integrity": "sha512-taxjPoNJLajZa3z3JSxwgArRIi5lYy3nlkmemup8bo0AtC7QpKOOE+xQ5wtSXcSMZZMxbsgQHp7FoBTeIUHngA==",
|
||||
"version": "0.8.26",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.26.tgz",
|
||||
"integrity": "sha512-q0Vcd5usphR5nghfyFksVx+VM+eSB1MyX8Ne304KFDnr214KQMA6DAjrEQJlGBUUCybLiOtPCvd3dxPecEQiSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
@@ -2460,9 +2459,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz",
|
||||
"integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==",
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz",
|
||||
"integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -3851,13 +3850,6 @@
|
||||
"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",
|
||||
@@ -3993,13 +3985,6 @@
|
||||
"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",
|
||||
@@ -4014,21 +3999,6 @@
|
||||
"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",
|
||||
@@ -4041,13 +4011,6 @@
|
||||
"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",
|
||||
@@ -4673,13 +4636,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@webgpu/types": {
|
||||
"version": "0.1.51",
|
||||
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.51.tgz",
|
||||
"integrity": "sha512-ktR3u64NPjwIViNCck+z9QeyN0iPkQCUOQ07ZCV1RzlkfP+olLTeEZ95O1QHS+v4w9vJeY9xj/uJuSphsHy5rQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@xterm/addon-fit": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
|
||||
@@ -5851,11 +5807,10 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
@@ -6823,13 +6778,6 @@
|
||||
"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",
|
||||
@@ -10119,19 +10067,11 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/meshoptimizer": {
|
||||
"version": "0.18.1",
|
||||
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
|
||||
"integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
|
||||
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
@@ -12012,12 +11952,6 @@
|
||||
"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",
|
||||
@@ -12236,6 +12170,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tsc-files": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/tsc-files/-/tsc-files-1.1.4.tgz",
|
||||
"integrity": "sha512-RePsRsOLru3BPpnf237y1Xe1oCGta8rmSYzM76kYo5tLGsv5R2r3s64yapYorGTPuuLyfS9NVbh9ydzmvNie2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"tsc-files": "cli.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=3"
|
||||
}
|
||||
},
|
||||
"node_modules/tsconfig": {
|
||||
"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.4.9",
|
||||
"version": "1.3.43",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -35,7 +35,6 @@
|
||||
"@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",
|
||||
@@ -59,6 +58,7 @@
|
||||
"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.3.6",
|
||||
"@comfyorg/litegraph": "^0.8.35",
|
||||
"@comfyorg/comfyui-electron-types": "^0.2.16",
|
||||
"@comfyorg/litegraph": "^0.8.26",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
@@ -87,11 +87,14 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 373 B |
|
Before Width: | Height: | Size: 410 B |
@@ -26,10 +26,7 @@ const betaMenuEnabled = computed(
|
||||
|
||||
const workflowStore = useWorkflowStore()
|
||||
const isUnsavedText = computed(() =>
|
||||
workflowStore.activeWorkflow?.isModified ||
|
||||
!workflowStore.activeWorkflow?.isPersisted
|
||||
? ' *'
|
||||
: ''
|
||||
workflowStore.activeWorkflow?.isModified ? ' *' : ''
|
||||
)
|
||||
const workflowNameText = computed(() => {
|
||||
const workflowName = workflowStore.activeWorkflow?.filename
|
||||
|
||||
@@ -125,45 +125,30 @@ const adjustMenuPosition = () => {
|
||||
const menuWidth = panelRef.value.offsetWidth
|
||||
const menuHeight = panelRef.value.offsetHeight
|
||||
|
||||
// Calculate distances to all edges
|
||||
const distanceLeft = lastDragState.value.x
|
||||
// Calculate the distance from each edge
|
||||
const distanceRight =
|
||||
lastDragState.value.windowWidth - (lastDragState.value.x + menuWidth)
|
||||
const distanceTop = lastDragState.value.y
|
||||
const distanceBottom =
|
||||
lastDragState.value.windowHeight - (lastDragState.value.y + menuHeight)
|
||||
|
||||
// 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
|
||||
)
|
||||
// Determine if the menu is closer to right/bottom or left/top
|
||||
const anchorRight = distanceRight < lastDragState.value.x
|
||||
const anchorBottom = distanceBottom < lastDragState.value.y
|
||||
|
||||
// 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
|
||||
|
||||
// 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
|
||||
// Calculate new position
|
||||
if (anchorRight) {
|
||||
x.value =
|
||||
screenWidth - (lastDragState.value.windowWidth - lastDragState.value.x)
|
||||
} else {
|
||||
// bottom
|
||||
x.value = horizontalRatio * screenWidth
|
||||
y.value = screenHeight - menuHeight - closestEdge.distance // Maintain exact distance from bottom
|
||||
x.value = lastDragState.value.x
|
||||
}
|
||||
|
||||
if (anchorBottom) {
|
||||
y.value =
|
||||
screenHeight -
|
||||
(lastDragState.value.windowHeight - lastDragState.value.y)
|
||||
} else {
|
||||
y.value = lastDragState.value.y
|
||||
}
|
||||
|
||||
// Ensure the menu stays within the screen bounds
|
||||
|
||||
104
src/components/bottomPanel/tabs/IntegratedTerminal.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="relative h-full w-full bg-black">
|
||||
<p v-if="errorMessage" class="p-4 text-center">{{ errorMessage }}</p>
|
||||
<ProgressSpinner
|
||||
v-else-if="loading"
|
||||
class="absolute inset-0 flex justify-center items-center h-full z-10"
|
||||
/>
|
||||
<div v-show="!loading" class="p-terminal rounded-none h-full w-full p-2">
|
||||
<div class="h-full" ref="terminalEl"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import '@xterm/xterm/css/xterm.css'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
import { api } from '@/scripts/api'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { debounce } from 'lodash'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { until } from '@vueuse/core'
|
||||
import { LogEntry, LogsWsMessage, TerminalSize } from '@/types/apiTypes'
|
||||
|
||||
const errorMessage = ref('')
|
||||
const loading = ref(true)
|
||||
const terminalEl = ref<HTMLDivElement>()
|
||||
const fitAddon = new FitAddon()
|
||||
const terminal = new Terminal({
|
||||
convertEol: true
|
||||
})
|
||||
terminal.loadAddon(fitAddon)
|
||||
|
||||
const resizeTerminal = () =>
|
||||
terminal.resize(terminal.cols, fitAddon.proposeDimensions().rows)
|
||||
|
||||
const resizeObserver = new ResizeObserver(debounce(resizeTerminal, 50))
|
||||
|
||||
const update = (entries: Array<LogEntry>, size?: TerminalSize) => {
|
||||
if (size) {
|
||||
terminal.resize(size.cols, fitAddon.proposeDimensions().rows)
|
||||
}
|
||||
terminal.write(entries.map((e) => e.m).join(''))
|
||||
}
|
||||
|
||||
const logReceived = (e: CustomEvent<LogsWsMessage>) => {
|
||||
update(e.detail.entries, e.detail.size)
|
||||
}
|
||||
|
||||
const loadLogEntries = async () => {
|
||||
const logs = await api.getRawLogs()
|
||||
update(logs.entries, logs.size)
|
||||
}
|
||||
|
||||
const watchLogs = async () => {
|
||||
const { clientId } = storeToRefs(useExecutionStore())
|
||||
if (!clientId.value) {
|
||||
await until(clientId).not.toBeNull()
|
||||
}
|
||||
api.subscribeLogs(true)
|
||||
api.addEventListener('logs', logReceived)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
terminal.open(terminalEl.value)
|
||||
|
||||
try {
|
||||
await loadLogEntries()
|
||||
} catch (err) {
|
||||
console.error('Error loading logs', err)
|
||||
// On older backends the endpoints wont exist
|
||||
errorMessage.value =
|
||||
'Unable to load logs, please ensure you have updated your ComfyUI backend.'
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
resizeObserver.observe(terminalEl.value)
|
||||
|
||||
await watchLogs()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (api.clientId) {
|
||||
api.subscribeLogs(false)
|
||||
}
|
||||
api.removeEventListener('logs', logReceived)
|
||||
|
||||
resizeObserver.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-terminal) .xterm {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
:deep(.p-terminal) .xterm-screen {
|
||||
background-color: black;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -1,30 +0,0 @@
|
||||
<template>
|
||||
<div class="relative h-full w-full bg-black" ref="rootEl">
|
||||
<div class="p-terminal rounded-none h-full w-full p-2">
|
||||
<div class="h-full terminal-host" ref="terminalEl"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, Ref } from 'vue'
|
||||
import { useTerminal } from '@/hooks/bottomPanelTabs/useTerminal'
|
||||
|
||||
const emit = defineEmits<{
|
||||
created: [ReturnType<typeof useTerminal>, Ref<HTMLElement>]
|
||||
}>()
|
||||
const terminalEl = ref<HTMLElement>()
|
||||
const rootEl = ref<HTMLElement>()
|
||||
emit('created', useTerminal(terminalEl), rootEl)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-terminal) .xterm {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
:deep(.p-terminal) .xterm-screen {
|
||||
background-color: black;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -1,73 +0,0 @@
|
||||
<template>
|
||||
<BaseTerminal @created="terminalCreated" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, Ref } from 'vue'
|
||||
import type { useTerminal } from '@/hooks/bottomPanelTabs/useTerminal'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import { IDisposable } from '@xterm/xterm'
|
||||
import BaseTerminal from './BaseTerminal.vue'
|
||||
|
||||
const terminalCreated = (
|
||||
{ terminal, useAutoSize }: ReturnType<typeof useTerminal>,
|
||||
root: Ref<HTMLElement>
|
||||
) => {
|
||||
// TODO: use types from electron package
|
||||
const terminalApi = electronAPI()['Terminal'] as {
|
||||
onOutput(cb: (message: string) => void): () => void
|
||||
resize(cols: number, rows: number): void
|
||||
restore(): Promise<{
|
||||
buffer: string[]
|
||||
pos: { x: number; y: number }
|
||||
size: { cols: number; rows: number }
|
||||
}>
|
||||
storePos(x: number, y: number): void
|
||||
write(data: string): void
|
||||
}
|
||||
|
||||
let offData: IDisposable
|
||||
let offOutput: () => void
|
||||
|
||||
useAutoSize(root, true, true, () => {
|
||||
// If we aren't visible, don't resize
|
||||
if (!terminal.element?.offsetParent) return
|
||||
|
||||
terminalApi.resize(terminal.cols, terminal.rows)
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
offData = terminal.onData(async (message: string) => {
|
||||
terminalApi.write(message)
|
||||
})
|
||||
|
||||
offOutput = terminalApi.onOutput((message) => {
|
||||
terminal.write(message)
|
||||
})
|
||||
|
||||
const restore = await terminalApi.restore()
|
||||
setTimeout(() => {
|
||||
if (restore.buffer.length) {
|
||||
terminal.resize(restore.size.cols, restore.size.rows)
|
||||
terminal.write(restore.buffer.join(''))
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
offData?.dispose()
|
||||
offOutput?.()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-terminal) .xterm {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
:deep(.p-terminal) .xterm-screen {
|
||||
background-color: black;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -1,90 +0,0 @@
|
||||
<template>
|
||||
<div class="bg-black h-full w-full">
|
||||
<p v-if="errorMessage" class="p-4 text-center">{{ errorMessage }}</p>
|
||||
<ProgressSpinner
|
||||
v-else-if="loading"
|
||||
class="relative inset-0 flex justify-center items-center h-full z-10"
|
||||
/>
|
||||
<BaseTerminal v-show="!loading" @created="terminalCreated" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, Ref, ref } from 'vue'
|
||||
import type { useTerminal } from '@/hooks/bottomPanelTabs/useTerminal'
|
||||
import { LogEntry, LogsWsMessage, TerminalSize } from '@/types/apiTypes'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { until } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import BaseTerminal from './BaseTerminal.vue'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
|
||||
const errorMessage = ref('')
|
||||
const loading = ref(true)
|
||||
|
||||
const terminalCreated = (
|
||||
{ terminal, useAutoSize }: ReturnType<typeof useTerminal>,
|
||||
root: Ref<HTMLElement>
|
||||
) => {
|
||||
useAutoSize(root, true, false)
|
||||
|
||||
const update = (entries: Array<LogEntry>, size?: TerminalSize) => {
|
||||
if (size) {
|
||||
terminal.resize(size.cols, terminal.rows)
|
||||
}
|
||||
terminal.write(entries.map((e) => e.m).join(''))
|
||||
}
|
||||
|
||||
const logReceived = (e: CustomEvent<LogsWsMessage>) => {
|
||||
update(e.detail.entries, e.detail.size)
|
||||
}
|
||||
|
||||
const loadLogEntries = async () => {
|
||||
const logs = await api.getRawLogs()
|
||||
update(logs.entries, logs.size)
|
||||
}
|
||||
|
||||
const watchLogs = async () => {
|
||||
const { clientId } = storeToRefs(useExecutionStore())
|
||||
if (!clientId.value) {
|
||||
await until(clientId).not.toBeNull()
|
||||
}
|
||||
api.subscribeLogs(true)
|
||||
api.addEventListener('logs', logReceived)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await loadLogEntries()
|
||||
} catch (err) {
|
||||
console.error('Error loading logs', err)
|
||||
// On older backends the endpoints wont exist
|
||||
errorMessage.value =
|
||||
'Unable to load logs, please ensure you have updated your ComfyUI backend.'
|
||||
return
|
||||
}
|
||||
|
||||
await watchLogs()
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (api.clientId) {
|
||||
api.subscribeLogs(false)
|
||||
}
|
||||
api.removeEventListener('logs', logReceived)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-terminal) .xterm {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
:deep(.p-terminal) .xterm-screen {
|
||||
background-color: black;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -28,14 +28,7 @@
|
||||
class="flex flex-row items-center gap-2"
|
||||
v-if="status === 'in_progress' || status === 'paused'"
|
||||
>
|
||||
<!-- 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"
|
||||
/>
|
||||
<ProgressBar class="flex-1" :value="downloadProgress" />
|
||||
|
||||
<Button
|
||||
class="file-action-button"
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
<!-- 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,7 +13,6 @@
|
||||
: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 py-0 px-2 2xl:px-4"
|
||||
class="tree-explorer p-2 2xl:p-4"
|
||||
:class="props.class"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
v-model:selectionKeys="selectionKeys"
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
</template>
|
||||
|
||||
<div class="action-container">
|
||||
<ReportIssueButton v-if="showSendError" :error="props.error" />
|
||||
<FindIssueButton
|
||||
:errorMessage="props.error.exception_message"
|
||||
:repoOwner="repoOwner"
|
||||
@@ -45,11 +44,9 @@ 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
|
||||
@@ -62,7 +59,6 @@ const reportOpen = ref(false)
|
||||
const showReport = () => {
|
||||
reportOpen.value = true
|
||||
}
|
||||
const showSendError = isElectron()
|
||||
|
||||
const toast = useToast()
|
||||
const { copy, isSupported } = useClipboard()
|
||||
|
||||
@@ -71,14 +71,6 @@
|
||||
</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>
|
||||
@@ -101,7 +93,6 @@ 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')
|
||||
@@ -109,9 +100,6 @@ const KeybindingPanel = defineAsyncComponent(
|
||||
const ExtensionPanel = defineAsyncComponent(
|
||||
() => import('./setting/ExtensionPanel.vue')
|
||||
)
|
||||
const ServerConfigPanel = defineAsyncComponent(
|
||||
() => import('./setting/ServerConfigPanel.vue')
|
||||
)
|
||||
|
||||
interface ISettingGroup {
|
||||
label: string
|
||||
@@ -136,33 +124,18 @@ 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)
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
<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>
|
||||
@@ -1,43 +0,0 @@
|
||||
<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,15 +7,45 @@
|
||||
:key="setting.id"
|
||||
class="setting-item flex items-center mb-4"
|
||||
>
|
||||
<SettingItem :setting="setting" />
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Divider from 'primevue/divider'
|
||||
import SettingItem from '@/components/dialog/content/setting/SettingItem.vue'
|
||||
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 { formatCamelCase } from '@/utils/formatUtil'
|
||||
|
||||
defineProps<{
|
||||
@@ -25,4 +55,67 @@ 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>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<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,8 +47,7 @@ import {
|
||||
DragAndScale,
|
||||
LGraphCanvas,
|
||||
ContextMenu,
|
||||
LGraphBadge,
|
||||
CanvasPointer
|
||||
LGraphBadge
|
||||
} from '@comfyorg/litegraph'
|
||||
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
@@ -62,7 +61,6 @@ 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)
|
||||
@@ -163,28 +161,6 @@ watchEffect(() => {
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
CanvasPointer.doubleClickTime = settingStore.get(
|
||||
'Comfy.Pointer.DoubleClickTime'
|
||||
)
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
CanvasPointer.bufferTime = settingStore.get('Comfy.Pointer.ClickBufferTime')
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
CanvasPointer.maxClickDrift = settingStore.get('Comfy.Pointer.ClickDrift')
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
LiteGraph.CANVAS_GRID_SIZE = settingStore.get('Comfy.SnapToGrid.GridSize')
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
LiteGraph.alwaysSnapToGrid = settingStore.get('pysssss.SnapToGrid')
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (!canvasStore.canvas) return
|
||||
|
||||
@@ -202,26 +178,13 @@ 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
|
||||
@@ -299,7 +262,6 @@ 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,14 +85,12 @@ onMounted(async () => {
|
||||
appData.value = paths.appData
|
||||
appPath.value = paths.appPath
|
||||
installPath.value = paths.defaultInstallPath
|
||||
|
||||
await validatePath(paths.defaultInstallPath)
|
||||
})
|
||||
|
||||
const validatePath = async (path: string) => {
|
||||
const validatePath = async () => {
|
||||
try {
|
||||
pathError.value = ''
|
||||
const validation = await electron.validateInstallPath(path)
|
||||
const validation = await electron.validateInstallPath(installPath.value)
|
||||
|
||||
if (!validation.isValid) {
|
||||
pathError.value = validation.error
|
||||
@@ -107,7 +105,7 @@ const browsePath = async () => {
|
||||
const result = await electron.showDirectoryPicker()
|
||||
if (result) {
|
||||
installPath.value = result
|
||||
await validatePath(result)
|
||||
await validatePath()
|
||||
}
|
||||
} catch (error) {
|
||||
pathError.value = t('install.failedToSelectDirectory')
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
<template>
|
||||
<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"
|
||||
>
|
||||
<div class="comfy-vue-node-search-container">
|
||||
<div class="comfy-vue-node-preview-container" v-if="enableNodePreview">
|
||||
<NodePreview
|
||||
:nodeDef="hoveredSuggestion"
|
||||
:key="hoveredSuggestion?.name || ''"
|
||||
@@ -16,10 +11,10 @@
|
||||
<Button
|
||||
icon="pi pi-filter"
|
||||
severity="secondary"
|
||||
class="filter-button z-10"
|
||||
class="_filter-button"
|
||||
@click="nodeSearchFilterVisible = true"
|
||||
/>
|
||||
<Dialog v-model:visible="nodeSearchFilterVisible" class="min-w-96">
|
||||
<Dialog v-model:visible="nodeSearchFilterVisible" class="_dialog">
|
||||
<template #header>
|
||||
<h3>Add node filter condition</h3>
|
||||
</template>
|
||||
@@ -30,7 +25,7 @@
|
||||
|
||||
<AutoCompletePlus
|
||||
:model-value="props.filters"
|
||||
class="comfy-vue-node-search-box z-10 flex-grow"
|
||||
class="comfy-vue-node-search-box"
|
||||
scrollHeight="40vh"
|
||||
:placeholder="placeholder"
|
||||
:input-id="inputId"
|
||||
@@ -153,3 +148,31 @@ 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"
|
||||
class="model-lib-tree-explorer py-0"
|
||||
:roots="renderedRoot.children"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
class="m-2"
|
||||
/>
|
||||
<TreeExplorer
|
||||
class="node-lib-tree-explorer"
|
||||
class="node-lib-tree-explorer py-0"
|
||||
:roots="renderedRoot.children"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
>
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
<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'
|
||||
@@ -111,7 +99,6 @@ 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'
|
||||
@@ -124,9 +111,7 @@ import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
const SETTING_FIT = 'Comfy.Queue.ImageFit'
|
||||
const SETTING_FLAT = 'Comfy.Queue.ShowFlatList'
|
||||
const SETTING_FILTER = 'Comfy.Queue.Filter'
|
||||
const IMAGE_FIT = 'Comfy.Queue.ImageFit'
|
||||
const confirm = useConfirm()
|
||||
const toast = useToast()
|
||||
const queueStore = useQueueStore()
|
||||
@@ -135,7 +120,7 @@ const commandStore = useCommandStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
// Expanded view: show all outputs in a flat list.
|
||||
const isExpanded = computed<boolean>(() => settingStore.get(SETTING_FLAT))
|
||||
const isExpanded = ref(false)
|
||||
const visibleTasks = ref<TaskItemImpl[]>([])
|
||||
const scrollContainer = ref<HTMLElement | null>(null)
|
||||
const loadMoreTrigger = ref<HTMLElement | null>(null)
|
||||
@@ -143,27 +128,10 @@ 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(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 imageFit = computed<string>(() => settingStore.get(IMAGE_FIT))
|
||||
|
||||
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
|
||||
@@ -181,48 +149,21 @@ const allGalleryItems = computed(() =>
|
||||
})
|
||||
)
|
||||
|
||||
const filterTasks = (tasks: TaskItemImpl[]) =>
|
||||
tasks.filter((t) => {
|
||||
if (
|
||||
hideCanceled.value &&
|
||||
t.status?.messages?.at(-1)?.[0] === 'execution_interrupted'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
hideCached.value &&
|
||||
t.flatOutputs?.length &&
|
||||
t.flatOutputs.every((o) => o.cached)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
const loadMoreItems = (iteration: number) => {
|
||||
const loadMoreItems = () => {
|
||||
const currentLength = visibleTasks.value.length
|
||||
const newTasks = filterTasks(allTasks.value).slice(
|
||||
const newTasks = 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 = (iteration: number) => {
|
||||
const checkAndLoadMore = () => {
|
||||
if (!scrollContainer.value) return
|
||||
|
||||
const { scrollHeight, scrollTop, clientHeight } = scrollContainer.value
|
||||
if (scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD) {
|
||||
loadMoreItems(iteration)
|
||||
loadMoreItems()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +171,7 @@ useInfiniteScroll(
|
||||
scrollContainer,
|
||||
() => {
|
||||
if (visibleTasks.value.length < allTasks.value.length) {
|
||||
loadMoreItems(0)
|
||||
loadMoreItems()
|
||||
}
|
||||
},
|
||||
{ distance: SCROLL_THRESHOLD }
|
||||
@@ -240,20 +181,16 @@ useInfiniteScroll(
|
||||
// This is necessary as the sidebar tab can change size when user drags the splitter.
|
||||
useResizeObserver(scrollContainer, () => {
|
||||
nextTick(() => {
|
||||
checkAndLoadMore(0)
|
||||
checkAndLoadMore()
|
||||
})
|
||||
})
|
||||
|
||||
const updateVisibleTasks = () => {
|
||||
visibleTasks.value = filterTasks(allTasks.value).slice(0, ITEMS_PER_PAGE)
|
||||
|
||||
nextTick(() => {
|
||||
checkAndLoadMore(0)
|
||||
})
|
||||
visibleTasks.value = allTasks.value.slice(0, ITEMS_PER_PAGE)
|
||||
}
|
||||
|
||||
const toggleExpanded = () => {
|
||||
settingStore.set(SETTING_FLAT, !isExpanded.value)
|
||||
isExpanded.value = !isExpanded.value
|
||||
updateVisibleTasks()
|
||||
}
|
||||
|
||||
@@ -354,10 +291,7 @@ const exitFolderView = () => {
|
||||
}
|
||||
|
||||
const toggleImageFit = () => {
|
||||
settingStore.set(
|
||||
SETTING_FIT,
|
||||
imageFit.value === 'cover' ? 'contain' : 'cover'
|
||||
)
|
||||
settingStore.set(IMAGE_FIT, imageFit.value === 'cover' ? 'contain' : 'cover')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -50,13 +50,10 @@
|
||||
<template #node="{ node }">
|
||||
<TreeExplorerTreeNode :node="node">
|
||||
<template #before-label="{ node }">
|
||||
<span v-if="node.data.isModified || !node.data.isPersisted"
|
||||
>*</span
|
||||
>
|
||||
<span v-if="node.data.isModified">*</span>
|
||||
</template>
|
||||
<template #actions="{ node }">
|
||||
<Button
|
||||
class="close-workflow-button"
|
||||
icon="pi pi-times"
|
||||
text
|
||||
:severity="
|
||||
|
||||
@@ -16,13 +16,9 @@
|
||||
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"
|
||||
class="node-lib-bookmark-tree-explorer py-0"
|
||||
ref="treeExplorerRef"
|
||||
:roots="renderedBookmarkedRoot.children"
|
||||
:expandedKeys="expandedKeys"
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="flex items-center gap-2">
|
||||
{{ $t('sideToolbar.queueTab.filters.hideCached') }}
|
||||
<ToggleSwitch v-model="hideCached" />
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
{{ $t('sideToolbar.queueTab.filters.hideCanceled') }}
|
||||
<ToggleSwitch v-model="hideCanceled" />
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
const SETTING_FILTER = 'Comfy.Queue.Filter'
|
||||
|
||||
const { t } = useI18n()
|
||||
const settingStore = useSettingStore()
|
||||
const filter = settingStore.get(SETTING_FILTER) ?? {}
|
||||
|
||||
const createCompute = (k: string) =>
|
||||
computed({
|
||||
get() {
|
||||
return filter[k]
|
||||
},
|
||||
set(value) {
|
||||
filter[k] = value
|
||||
settingStore.set(SETTING_FILTER, filter)
|
||||
}
|
||||
})
|
||||
|
||||
const hideCached = createCompute('hideCached')
|
||||
const hideCanceled = createCompute('hideCanceled')
|
||||
</script>
|
||||
@@ -31,20 +31,16 @@
|
||||
|
||||
<div class="task-item-details">
|
||||
<div class="tag-wrapper status-tag-group">
|
||||
<Tag v-if="isFlatTask && task.isHistory && node" class="node-name-tag">
|
||||
<Tag v-if="isFlatTask && task.isHistory" class="node-name-tag">
|
||||
<Button
|
||||
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)"
|
||||
class="task-duration relative"
|
||||
>
|
||||
<i v-if="isCachedResult" class="pi pi-server task-cached-icon"></i>
|
||||
<Tag :severity="taskTagSeverity(task.displayStatus)">
|
||||
<span v-html="taskStatusText(task.displayStatus)"></span>
|
||||
<span v-if="task.isHistory" class="task-time">
|
||||
{{ formatTime(task.executionTimeInSeconds) }}
|
||||
@@ -94,7 +90,6 @@ const node: ComfyNode | null =
|
||||
) ?? null
|
||||
: null
|
||||
const progressPreviewBlobUrl = ref('')
|
||||
const isCachedResult = props.isFlatTask && coverResult?.cached
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
@@ -147,7 +142,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${isCachedResult ? ' cached' : ''}" style="font-weight: bold"></i>`
|
||||
return '<i class="pi pi-check" style="font-weight: bold"></i>'
|
||||
case TaskItemDisplayStatus.Failed:
|
||||
return 'Failed'
|
||||
case TaskItemDisplayStatus.Cancelled:
|
||||
@@ -231,15 +226,4 @@ 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,10 +18,7 @@
|
||||
<div class="relative">
|
||||
<span
|
||||
class="status-indicator"
|
||||
v-if="
|
||||
!workspaceStore.shiftDown &&
|
||||
(option.workflow.isModified || !option.workflow.isPersisted)
|
||||
"
|
||||
v-if="!workspaceStore.shiftDown && option.workflow.isModified"
|
||||
>•</span
|
||||
>
|
||||
<Button
|
||||
|
||||
@@ -1,432 +0,0 @@
|
||||
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: ['Appearance', 'ColorPalette'],
|
||||
category: ['Comfy', 'ColorPalette'],
|
||||
name: 'Color Palette',
|
||||
type: (name, setter, value) => {
|
||||
const options = [
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
import { app } from '@/scripts/app'
|
||||
import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
|
||||
;(async () => {
|
||||
if (!isElectron()) return
|
||||
|
||||
const electronAPI = getElectronAPI()
|
||||
const desktopAppVersion = await electronAPI.getElectronVersion()
|
||||
|
||||
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,7 +13,6 @@ import {
|
||||
deserialiseAndCreate,
|
||||
serialise
|
||||
} from '@/extensions/core/vintageClipboard'
|
||||
import type { ComfyNodeDef } from '@/types/apiTypes'
|
||||
|
||||
type GroupNodeWorkflowData = {
|
||||
external: ComfyLink[]
|
||||
@@ -57,7 +56,7 @@ const Workflow = {
|
||||
|
||||
class GroupNodeBuilder {
|
||||
nodes: LGraphNode[]
|
||||
nodeData: GroupNodeWorkflowData
|
||||
nodeData: any
|
||||
|
||||
constructor(nodes: LGraphNode[]) {
|
||||
this.nodes = nodes
|
||||
@@ -176,7 +175,7 @@ export class GroupNodeConfig {
|
||||
primitiveToWidget: {}
|
||||
nodeInputs: {}
|
||||
outputVisibility: any[]
|
||||
nodeDef: ComfyNodeDef
|
||||
nodeDef: any
|
||||
inputs: any[]
|
||||
linksFrom: {}
|
||||
linksTo: {}
|
||||
@@ -205,7 +204,6 @@ 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,
|
||||
@@ -697,11 +695,11 @@ export class GroupNodeConfig {
|
||||
}
|
||||
|
||||
export class GroupNodeHandler {
|
||||
node: LGraphNode
|
||||
node
|
||||
groupData
|
||||
innerNodes: any
|
||||
|
||||
constructor(node: LGraphNode) {
|
||||
constructor(node) {
|
||||
this.node = node
|
||||
this.groupData = node.constructor?.nodeData?.[GROUP]
|
||||
|
||||
@@ -776,7 +774,6 @@ 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]
|
||||
@@ -968,20 +965,17 @@ export class GroupNodeHandler {
|
||||
|
||||
app.canvas.emitBeforeChange()
|
||||
|
||||
try {
|
||||
const { newNodes, selectedIds } = addInnerNodes()
|
||||
reconnectInputs(selectedIds)
|
||||
reconnectOutputs(selectedIds)
|
||||
app.graph.remove(this.node)
|
||||
const { newNodes, selectedIds } = addInnerNodes()
|
||||
reconnectInputs(selectedIds)
|
||||
reconnectOutputs(selectedIds)
|
||||
app.graph.remove(this.node)
|
||||
|
||||
return newNodes
|
||||
} finally {
|
||||
app.canvas.emitAfterChange()
|
||||
}
|
||||
app.canvas.emitAfterChange()
|
||||
|
||||
return newNodes
|
||||
}
|
||||
|
||||
const getExtraMenuOptions = this.node.getExtraMenuOptions
|
||||
// @ts-expect-error Should pass patched return value getExtraMenuOptions
|
||||
this.node.getExtraMenuOptions = function (_, options) {
|
||||
getExtraMenuOptions?.apply(this, arguments)
|
||||
|
||||
@@ -994,7 +988,6 @@ export class GroupNodeHandler {
|
||||
null,
|
||||
{
|
||||
content: 'Convert to nodes',
|
||||
// @ts-expect-error
|
||||
callback: () => {
|
||||
return this.convertToNodes()
|
||||
}
|
||||
@@ -1155,7 +1148,6 @@ export class GroupNodeHandler {
|
||||
|
||||
if (
|
||||
old.inputName !== 'image' &&
|
||||
// @ts-expect-error Widget values
|
||||
!widget.options.values.includes(widget.value)
|
||||
) {
|
||||
widget.value = widget.options.values[0]
|
||||
@@ -1362,7 +1354,6 @@ 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]
|
||||
)
|
||||
@@ -1484,7 +1475,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,7 +4,6 @@ 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
|
||||
@@ -12,8 +11,7 @@ function setNodeMode(node: LGraphNode, mode: number) {
|
||||
}
|
||||
|
||||
function addNodesToGroup(group: LGraphGroup, items: Iterable<Positionable>) {
|
||||
const padding = useSettingStore().get('Comfy.GroupSelectedNodes.Padding')
|
||||
group.resizeTo([...group.children, ...items], padding)
|
||||
group.resizeTo([...group.children, ...items])
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
@@ -78,10 +76,7 @@ app.registerExtension({
|
||||
content: 'Fit Group To Nodes',
|
||||
callback: () => {
|
||||
group.recomputeInsideNodes()
|
||||
const padding = useSettingStore().get(
|
||||
'Comfy.GroupSelectedNodes.Padding'
|
||||
)
|
||||
group.resizeTo(group.children, padding)
|
||||
group.resizeTo(group.children)
|
||||
this.graph.change()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -15,9 +15,8 @@ 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: ['LiteGraph', 'Menu', 'InvertMenuScrolling'],
|
||||
category: ['Comfy', 'Graph', 'InvertMenuScrolling'],
|
||||
name: 'Invert Context Menu Scrolling',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
|
||||
@@ -44,7 +44,7 @@ app.registerExtension({
|
||||
lastTouch = getMultiTouchCenter(e)
|
||||
|
||||
touchDist = getMultiTouchPos(e)
|
||||
app.canvas.pointer.isDown = false
|
||||
app.canvas.pointer_is_down = 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.isDown = false
|
||||
app.canvas.pointer_is_down = false
|
||||
touchZooming = true
|
||||
|
||||
LiteGraph.closeAllContextMenus(window)
|
||||
@@ -137,7 +137,7 @@ LGraphCanvas.prototype.processMouseDown = function (e) {
|
||||
if (touchZooming || touchCount) {
|
||||
return
|
||||
}
|
||||
app.canvas.pointer.isDown = false // Prevent context menu from opening on second tap
|
||||
app.canvas.pointer_is_down = false // Prevent context menu from opening on second tap
|
||||
return processMouseDown.apply(this, arguments)
|
||||
}
|
||||
|
||||
|
||||
202
src/extensions/core/snapToGrid.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
// @ts-strict-ignore
|
||||
import type { SettingParams } from '@/types/settingTypes'
|
||||
import { app } from '../../scripts/app'
|
||||
import {
|
||||
LGraphCanvas,
|
||||
LGraphNode,
|
||||
LGraphGroup,
|
||||
LiteGraph
|
||||
} from '@comfyorg/litegraph'
|
||||
|
||||
// Shift + drag/resize to snap to grid
|
||||
|
||||
/** Rounds a Vector2 in-place to the current CANVAS_GRID_SIZE. */
|
||||
function roundVectorToGrid(vec) {
|
||||
vec[0] =
|
||||
LiteGraph.CANVAS_GRID_SIZE * Math.round(vec[0] / LiteGraph.CANVAS_GRID_SIZE)
|
||||
vec[1] =
|
||||
LiteGraph.CANVAS_GRID_SIZE * Math.round(vec[1] / LiteGraph.CANVAS_GRID_SIZE)
|
||||
return vec
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Comfy.SnapToGrid',
|
||||
init() {
|
||||
// Add setting to control grid size
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.SnapToGrid.GridSize',
|
||||
category: ['Comfy', 'Graph', 'GridSize'],
|
||||
name: 'Snap to grid size',
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 1,
|
||||
max: 500
|
||||
},
|
||||
tooltip:
|
||||
'When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid.',
|
||||
defaultValue: LiteGraph.CANVAS_GRID_SIZE,
|
||||
onChange(value) {
|
||||
LiteGraph.CANVAS_GRID_SIZE = +value || 10
|
||||
}
|
||||
})
|
||||
// Keep the 'pysssss.SnapToGrid' setting id so we don't need to migrate setting values.
|
||||
// Using a new setting id can cause existing users to lose their existing settings.
|
||||
const alwaysSnapToGrid = app.ui.settings.addSetting({
|
||||
id: 'pysssss.SnapToGrid',
|
||||
category: ['Comfy', 'Graph', 'AlwaysSnapToGrid'],
|
||||
name: 'Always snap to grid',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
versionAdded: '1.3.13'
|
||||
} as SettingParams)
|
||||
|
||||
const shouldSnapToGrid = () => app.shiftDown || alwaysSnapToGrid.value
|
||||
|
||||
// After moving a node, if the shift key is down align it to grid
|
||||
const onNodeMoved = app.canvas.onNodeMoved
|
||||
app.canvas.onNodeMoved = function (node) {
|
||||
const r = onNodeMoved?.apply(this, arguments)
|
||||
|
||||
if (shouldSnapToGrid()) {
|
||||
// Ensure all selected nodes are realigned
|
||||
for (const id in this.selected_nodes) {
|
||||
this.selected_nodes[id].alignToGrid()
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// When a node is added, add a resize handler to it so we can fix align the size with the grid
|
||||
const onNodeAdded = app.graph.onNodeAdded
|
||||
app.graph.onNodeAdded = function (node) {
|
||||
const onResize = node.onResize
|
||||
node.onResize = function () {
|
||||
if (shouldSnapToGrid()) {
|
||||
roundVectorToGrid(node.size)
|
||||
}
|
||||
return onResize?.apply(this, arguments)
|
||||
}
|
||||
return onNodeAdded?.apply(this, arguments)
|
||||
}
|
||||
|
||||
// Draw a preview of where the node will go if holding shift and the node is selected
|
||||
const origDrawNode = LGraphCanvas.prototype.drawNode
|
||||
LGraphCanvas.prototype.drawNode = function (node, ctx) {
|
||||
if (
|
||||
shouldSnapToGrid() &&
|
||||
this.node_dragged &&
|
||||
node.id in this.selected_nodes
|
||||
) {
|
||||
const [x, y] = roundVectorToGrid([...node.pos])
|
||||
const shiftX = x - node.pos[0]
|
||||
let shiftY = y - node.pos[1]
|
||||
|
||||
let w, h
|
||||
if (node.flags.collapsed) {
|
||||
w = node._collapsed_width
|
||||
h = LiteGraph.NODE_TITLE_HEIGHT
|
||||
shiftY -= LiteGraph.NODE_TITLE_HEIGHT
|
||||
} else {
|
||||
w = node.size[0]
|
||||
h = node.size[1]
|
||||
const titleMode = node.constructor.title_mode
|
||||
if (
|
||||
titleMode !== LiteGraph.TRANSPARENT_TITLE &&
|
||||
titleMode !== LiteGraph.NO_TITLE
|
||||
) {
|
||||
h += LiteGraph.NODE_TITLE_HEIGHT
|
||||
shiftY -= LiteGraph.NODE_TITLE_HEIGHT
|
||||
}
|
||||
}
|
||||
const f = ctx.fillStyle
|
||||
ctx.fillStyle = 'rgba(100, 100, 100, 0.5)'
|
||||
ctx.fillRect(shiftX, shiftY, w, h)
|
||||
ctx.fillStyle = f
|
||||
}
|
||||
|
||||
return origDrawNode.apply(this, arguments)
|
||||
}
|
||||
|
||||
/**
|
||||
* The currently moving, selected group only. Set after the `selected_group` has actually started
|
||||
* moving.
|
||||
*/
|
||||
let selectedAndMovingGroup: LGraphGroup | null = null
|
||||
|
||||
/**
|
||||
* Handles moving a group; tracking when a group has been moved (to show the ghost in `drawGroups`
|
||||
* below) as well as handle the last move call from LiteGraph's `processMouseUp`.
|
||||
*/
|
||||
const groupMove = LGraphGroup.prototype.move
|
||||
LGraphGroup.prototype.move = function (deltax, deltay, ignore_nodes) {
|
||||
const v = groupMove.apply(this, arguments)
|
||||
// When we've started moving, set `selectedAndMovingGroup` as LiteGraph sets `selected_group`
|
||||
// too eagerly and we don't want to behave like we're moving until we get a delta.
|
||||
if (
|
||||
!selectedAndMovingGroup &&
|
||||
app.canvas.selected_group === this &&
|
||||
(deltax || deltay)
|
||||
) {
|
||||
selectedAndMovingGroup = this
|
||||
}
|
||||
|
||||
// LiteGraph will call group.move both on mouse-move as well as mouse-up though we only want
|
||||
// to snap on a mouse-up which we can determine by checking if `app.canvas.last_mouse_dragging`
|
||||
// has been set to `false`. Essentially, this check here is the equivalent to calling an
|
||||
// `LGraphGroup.prototype.onNodeMoved` if it had existed.
|
||||
if (app.canvas.last_mouse_dragging === false && shouldSnapToGrid()) {
|
||||
// After moving a group (while shouldSnapToGrid()), snap all the child nodes and, finally,
|
||||
// align the group itself.
|
||||
this.recomputeInsideNodes()
|
||||
for (const node of this.nodes) {
|
||||
node.alignToGrid()
|
||||
}
|
||||
LGraphNode.prototype.alignToGrid.apply(this)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles drawing a group when, snapping the size when one is actively being resized tracking and/or
|
||||
* drawing a ghost box when one is actively being moved. This mimics the node snapping behavior for
|
||||
* both.
|
||||
*/
|
||||
const drawGroups = LGraphCanvas.prototype.drawGroups
|
||||
LGraphCanvas.prototype.drawGroups = function (canvas, ctx) {
|
||||
if (this.selected_group && shouldSnapToGrid()) {
|
||||
if (this.selected_group_resizing) {
|
||||
roundVectorToGrid(this.selected_group.size)
|
||||
} else if (selectedAndMovingGroup) {
|
||||
const [x, y] = roundVectorToGrid([...selectedAndMovingGroup.pos])
|
||||
const f = ctx.fillStyle
|
||||
const s = ctx.strokeStyle
|
||||
ctx.fillStyle = 'rgba(100, 100, 100, 0.33)'
|
||||
ctx.strokeStyle = 'rgba(100, 100, 100, 0.66)'
|
||||
ctx.rect(x, y, ...(selectedAndMovingGroup.size as [number, number]))
|
||||
ctx.fill()
|
||||
ctx.stroke()
|
||||
ctx.fillStyle = f
|
||||
ctx.strokeStyle = s
|
||||
}
|
||||
} else if (!this.selected_group) {
|
||||
selectedAndMovingGroup = null
|
||||
}
|
||||
return drawGroups.apply(this, arguments)
|
||||
}
|
||||
|
||||
/** Handles adding a group in a snapping-enabled state. */
|
||||
const onGroupAdd = LGraphCanvas.onGroupAdd
|
||||
LGraphCanvas.onGroupAdd = function () {
|
||||
const v = onGroupAdd.apply(app.canvas, arguments)
|
||||
if (shouldSnapToGrid()) {
|
||||
const lastGroup = app.graph.groups[app.graph.groups.length - 1]
|
||||
if (lastGroup) {
|
||||
roundVectorToGrid(lastGroup.pos)
|
||||
roundVectorToGrid(lastGroup.size)
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -69,54 +69,51 @@ export function deserialiseAndCreate(data: string, canvas: LGraphCanvas): void {
|
||||
|
||||
const { graph, graph_mouse } = canvas
|
||||
canvas.emitBeforeChange()
|
||||
try {
|
||||
graph.beforeChange()
|
||||
graph.beforeChange()
|
||||
|
||||
const deserialised = JSON.parse(data)
|
||||
const deserialised = JSON.parse(data)
|
||||
|
||||
// Find the top left point of the boundary of all pasted nodes
|
||||
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()
|
||||
// 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()
|
||||
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)
|
||||
}
|
||||
|
||||
14
src/hooks/bottomPanelTabs/integratedTerminalTab.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { markRaw } from 'vue'
|
||||
import IntegratedTerminal from '@/components/bottomPanel/tabs/IntegratedTerminal.vue'
|
||||
import { BottomPanelExtension } from '@/types/extensionTypes'
|
||||
|
||||
export const useIntegratedTerminalTab = (): BottomPanelExtension => {
|
||||
const { t } = useI18n()
|
||||
return {
|
||||
id: 'integrated-terminal',
|
||||
title: t('terminal'),
|
||||
component: markRaw(IntegratedTerminal),
|
||||
type: 'vue'
|
||||
}
|
||||
}
|
||||