Compare commits
21 Commits
v1.10.4
...
flash-boar
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb3c415065 | ||
|
|
d3ab23a532 | ||
|
|
08a6867c00 | ||
|
|
dbbe67dfcd | ||
|
|
40fa1d37bc | ||
|
|
0d6bc669f5 | ||
|
|
e4444d4074 | ||
|
|
cbf5dff633 | ||
|
|
9de8450deb | ||
|
|
3b0e3d635b | ||
|
|
d1a682bc01 | ||
|
|
01ffc9e4eb | ||
|
|
54e42178f7 | ||
|
|
25e5ab3a36 | ||
|
|
28dd6a2702 | ||
|
|
3b3df250cd | ||
|
|
6441a86619 | ||
|
|
79db202925 | ||
|
|
f7556e0015 | ||
|
|
141e64354c | ||
|
|
79452ce267 |
63
browser_tests/assets/missing_nodes_converted_widget.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"last_node_id": 1,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "UNKNOWN NODE",
|
||||
"pos": [
|
||||
48,
|
||||
86
|
||||
],
|
||||
"size": {
|
||||
"0": 358.80780029296875,
|
||||
"1": 314.7989501953125
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": null,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "foo",
|
||||
"type": "STRING",
|
||||
"link": null,
|
||||
"slot_index": 1,
|
||||
"widget": {
|
||||
"name": "foo"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [],
|
||||
"slot_index": 0,
|
||||
"shape": 6
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UNKNOWN NODE"
|
||||
},
|
||||
"widgets_values": [
|
||||
"wd-v1-4-moat-tagger-v2",
|
||||
0.35,
|
||||
0.85,
|
||||
false,
|
||||
false,
|
||||
""
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -152,9 +152,10 @@ test.describe('Color Palette', () => {
|
||||
// doesn't update the store immediately.
|
||||
await comfyPage.setup()
|
||||
|
||||
await comfyPage.loadWorkflow('every_node_color')
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'custom-color-palette-obsidian-dark.png'
|
||||
'custom-color-palette-obsidian-dark-all-colors.png'
|
||||
)
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'light_red')
|
||||
await comfyPage.nextFrame()
|
||||
@@ -232,7 +233,7 @@ test.describe('Node Color Adjustments', () => {
|
||||
const workflow = await comfyPage.page.evaluate(() => {
|
||||
return localStorage.getItem('workflow')
|
||||
})
|
||||
for (const node of JSON.parse(workflow).nodes) {
|
||||
for (const node of JSON.parse(workflow ?? '{}').nodes) {
|
||||
if (node.bgcolor) expect(node.bgcolor).not.toMatch(/hsla/)
|
||||
if (node.color) expect(node.color).not.toMatch(/hsla/)
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 135 KiB |
@@ -1,3 +1,4 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { APIRequestContext, Locator, Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import { test as base } from '@playwright/test'
|
||||
@@ -646,6 +647,18 @@ export class ComfyPage {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async selectNodes(nodeTitles: string[]) {
|
||||
await this.page.keyboard.down('Control')
|
||||
for (const nodeTitle of nodeTitles) {
|
||||
const nodes = await this.getNodeRefsByTitle(nodeTitle)
|
||||
for (const node of nodes) {
|
||||
await node.click('title')
|
||||
}
|
||||
}
|
||||
await this.page.keyboard.up('Control')
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async select2Nodes() {
|
||||
// Select 2 CLIP nodes.
|
||||
await this.page.keyboard.down('Control')
|
||||
@@ -835,12 +848,24 @@ export class ComfyPage {
|
||||
(
|
||||
await this.page.evaluate((type) => {
|
||||
return window['app'].graph.nodes
|
||||
.filter((n) => n.type === type)
|
||||
.map((n) => n.id)
|
||||
.filter((n: LGraphNode) => n.type === type)
|
||||
.map((n: LGraphNode) => n.id)
|
||||
}, type)
|
||||
).map((id: NodeId) => this.getNodeRefById(id))
|
||||
)
|
||||
}
|
||||
async getNodeRefsByTitle(title: string): Promise<NodeReference[]> {
|
||||
return Promise.all(
|
||||
(
|
||||
await this.page.evaluate((title) => {
|
||||
return window['app'].graph.nodes
|
||||
.filter((n: LGraphNode) => n.title === title)
|
||||
.map((n: LGraphNode) => n.id)
|
||||
}, title)
|
||||
).map((id: NodeId) => this.getNodeRefById(id))
|
||||
)
|
||||
}
|
||||
|
||||
async getFirstNodeRef(): Promise<NodeReference | null> {
|
||||
const id = await this.page.evaluate(() => {
|
||||
return window['app'].graph.nodes[0]?.id
|
||||
@@ -896,9 +921,10 @@ export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
|
||||
try {
|
||||
await comfyPage.setupSettings({
|
||||
'Comfy.UseNewMenu': 'Disabled',
|
||||
// Hide canvas menu/info by default.
|
||||
// Hide canvas menu/info/selection toolbox by default.
|
||||
'Comfy.Graph.CanvasInfo': false,
|
||||
'Comfy.Graph.CanvasMenu': false,
|
||||
'Comfy.Canvas.SelectionToolbox': false,
|
||||
// Hide all badges by default.
|
||||
'Comfy.NodeBadge.NodeIdBadgeMode': NodeBadgeMode.None,
|
||||
'Comfy.NodeBadge.NodeSourceBadgeMode': NodeBadgeMode.None,
|
||||
|
||||
@@ -53,4 +53,11 @@ test.describe('Optional input', () => {
|
||||
await comfyPage.loadWorkflow('simple_slider')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('simple_slider.png')
|
||||
})
|
||||
test('unknown converted widget', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.Workflow.ShowMissingNodesWarning', false)
|
||||
await comfyPage.loadWorkflow('missing_nodes_converted_widget')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'missing_nodes_converted_widget.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 47 KiB |
@@ -176,6 +176,23 @@ test.describe('Remote COMBO Widget', () => {
|
||||
})
|
||||
|
||||
test.describe('Refresh Behavior', () => {
|
||||
test('refresh button is visible in selection toolbar when node is selected', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
|
||||
|
||||
const nodeName = 'Remote Widget Node'
|
||||
await addRemoteWidgetNode(comfyPage, nodeName)
|
||||
await waitForWidgetUpdate(comfyPage)
|
||||
|
||||
// Select remote widget node
|
||||
await comfyPage.page.keyboard.press('Control+A')
|
||||
|
||||
await expect(
|
||||
comfyPage.page.locator('.selection-toolbox .pi-refresh')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('refreshes options when TTL expires', async ({ comfyPage }) => {
|
||||
// Fulfill each request with a unique timestamp
|
||||
await comfyPage.page.route(
|
||||
|
||||
94
browser_tests/selectionToolbox.spec.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture } from './fixtures/ComfyPage'
|
||||
|
||||
const test = comfyPageFixture
|
||||
|
||||
test.describe('Selection Toolbox', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
|
||||
})
|
||||
|
||||
test('shows selection toolbox', async ({ comfyPage }) => {
|
||||
// By default, selection toolbox should be enabled
|
||||
expect(
|
||||
await comfyPage.page.locator('.selection-overlay-container').isVisible()
|
||||
).toBe(false)
|
||||
|
||||
// Select multiple nodes
|
||||
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
|
||||
|
||||
// Selection toolbox should be visible with multiple nodes selected
|
||||
await expect(
|
||||
comfyPage.page.locator('.selection-overlay-container')
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.page.locator('.selection-overlay-container.show-border')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('shows border only with multiple selections', async ({ comfyPage }) => {
|
||||
// Select single node
|
||||
await comfyPage.selectNodes(['KSampler'])
|
||||
|
||||
// Selection overlay should be visible but without border
|
||||
await expect(
|
||||
comfyPage.page.locator('.selection-overlay-container')
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.page.locator('.selection-overlay-container.show-border')
|
||||
).not.toBeVisible()
|
||||
|
||||
// Select multiple nodes
|
||||
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
|
||||
|
||||
// Selection overlay should show border with multiple selections
|
||||
await expect(
|
||||
comfyPage.page.locator('.selection-overlay-container.show-border')
|
||||
).toBeVisible()
|
||||
|
||||
// Deselect to single node
|
||||
await comfyPage.selectNodes(['CLIP Text Encode (Prompt)'])
|
||||
|
||||
// Border should be hidden again
|
||||
await expect(
|
||||
comfyPage.page.locator('.selection-overlay-container.show-border')
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('displays refresh button in toolbox when all nodes are selected', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Select all nodes
|
||||
await comfyPage.page.focus('canvas')
|
||||
await comfyPage.page.keyboard.press('Control+A')
|
||||
|
||||
await expect(
|
||||
comfyPage.page.locator('.selection-toolbox .pi-refresh')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('displays bypass button in toolbox when nodes are selected', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// A group + a KSampler node
|
||||
await comfyPage.loadWorkflow('single_group')
|
||||
|
||||
// Select group + node should show bypass button
|
||||
await comfyPage.page.focus('canvas')
|
||||
await comfyPage.page.keyboard.press('Control+A')
|
||||
await expect(
|
||||
comfyPage.page.locator(
|
||||
'.selection-toolbox *[data-testid="bypass-button"]'
|
||||
)
|
||||
).toBeVisible()
|
||||
|
||||
// Deselect node (Only group is selected) should hide bypass button
|
||||
await comfyPage.selectNodes(['KSampler'])
|
||||
await expect(
|
||||
comfyPage.page.locator(
|
||||
'.selection-toolbox *[data-testid="bypass-button"]'
|
||||
)
|
||||
).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
20
package-lock.json
generated
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.10.4",
|
||||
"version": "1.10.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.10.4",
|
||||
"version": "1.10.6",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.16",
|
||||
"@comfyorg/litegraph": "^0.8.83",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.20",
|
||||
"@comfyorg/litegraph": "^0.8.87",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
@@ -1938,15 +1938,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@comfyorg/comfyui-electron-types": {
|
||||
"version": "0.4.16",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.16.tgz",
|
||||
"integrity": "sha512-AKy4WLVAuDka/Xjv8zrKwfU/wfRSQpFVE5DgxoLfvroCI0sw+rV1JqdL6xFVrYIoeprzbfKhQiyqlAWU+QgHyg==",
|
||||
"version": "0.4.20",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.20.tgz",
|
||||
"integrity": "sha512-JFKGk9wSx7CcYh9MRNo7bqTLJwQzVc+1Xg8V2Ghn9BS3RzpmkfktaWHi+waU7/CRQMzvjF+mnDPP58xk1xbVhA==",
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.8.83",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.83.tgz",
|
||||
"integrity": "sha512-4ZfRk0mBcCStY2yRERCrguwFf5v6WajD/6/JEmycD3HnF4OwYgyAspMYrscJcQ/R2MXfnedGe1gi8WXQ955vEQ==",
|
||||
"version": "0.8.87",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.87.tgz",
|
||||
"integrity": "sha512-hEBe8Cc8C3PkWLfUxxhuO7zitYYCq3dO9mX8DfoK6On8EBE+1UijugVKfTWHuB/Yii4rN8yck/CI9yOYvCuD7Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.10.4",
|
||||
"version": "1.10.6",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -83,8 +83,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.16",
|
||||
"@comfyorg/litegraph": "^0.8.83",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.20",
|
||||
"@comfyorg/litegraph": "^0.8.87",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
|
||||
@@ -455,11 +455,6 @@
|
||||
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp16.safetensors?download=true",
|
||||
"directory": "text_encoders"
|
||||
},
|
||||
{
|
||||
"name": "flux1-dev-fp8.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/flux1-dev/resolve/main/flux1-dev-fp8.safetensors?download=true",
|
||||
"directory": "checkpoints"
|
||||
},
|
||||
{
|
||||
"name": "ae.safetensors",
|
||||
"url": "https://huggingface.co/black-forest-labs/FLUX.1-schnell/resolve/main/ae.safetensors?download=true",
|
||||
|
||||
@@ -430,11 +430,6 @@
|
||||
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp16.safetensors?download=true",
|
||||
"directory": "text_encoders"
|
||||
},
|
||||
{
|
||||
"name": "flux1-dev-fp8.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/flux1-dev/resolve/main/flux1-dev-fp8.safetensors?download=true",
|
||||
"directory": "checkpoints"
|
||||
},
|
||||
{
|
||||
"name": "ae.safetensors",
|
||||
"url": "https://huggingface.co/black-forest-labs/FLUX.1-schnell/resolve/main/ae.safetensors?download=true",
|
||||
|
||||
@@ -463,7 +463,7 @@
|
||||
"properties": {
|
||||
"Node name for S&R": "UNETLoader"
|
||||
},
|
||||
"widgets_values": ["flux1-dev-fp8.safetensors", "default"],
|
||||
"widgets_values": ["flux1-dev.safetensors", "default"],
|
||||
"color": "#223",
|
||||
"bgcolor": "#335"
|
||||
},
|
||||
@@ -542,7 +542,7 @@
|
||||
"text": ""
|
||||
},
|
||||
"widgets_values": [
|
||||
"If you get an error in any of the nodes above make sure the files are in the correct directories.\n\nSee the top of the examples page for the links : https://comfyanonymous.github.io/ComfyUI_examples/flux/\n\nflux1-dev-fp8.safetensors goes in: ComfyUI/models/unet/\n\nt5xxl_fp16.safetensors and clip_l.safetensors go in: ComfyUI/models/clip/\n\nae.safetensors goes in: ComfyUI/models/vae/\n\n\nTip: You can set the weight_dtype above to one of the fp8 types if you have memory issues."
|
||||
"If you get an error in any of the nodes above make sure the files are in the correct directories.\n\nSee the top of the examples page for the links : https://comfyanonymous.github.io/ComfyUI_examples/flux/\n\nflux1-dev.safetensors goes in: ComfyUI/models/unet/\n\nt5xxl_fp16.safetensors and clip_l.safetensors go in: ComfyUI/models/clip/\n\nae.safetensors goes in: ComfyUI/models/vae/\n\n\nTip: You can set the weight_dtype above to one of the fp8 types if you have memory issues."
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
@@ -763,8 +763,8 @@
|
||||
"directory": "vae"
|
||||
},
|
||||
{
|
||||
"name": "flux1-dev-fp8.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/flux1-dev/resolve/main/flux1-dev-fp8.safetensors?download=true",
|
||||
"name": "flux1-dev.safetensors",
|
||||
"url": "https://huggingface.co/black-forest-labs/FLUX.1-dev/resolve/main/flux1-dev.safetensors?download=true",
|
||||
"directory": "diffusion_models"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -473,7 +473,7 @@
|
||||
"text": ""
|
||||
},
|
||||
"widgets_values": [
|
||||
"If you get an error in any of the nodes above make sure the files are in the correct directories.\n\nSee the top of the examples page for the links : https://comfyanonymous.github.io/ComfyUI_examples/flux/\n\nflux1-dev.safetensors goes in: ComfyUI/models/unet/\n\nt5xxl_fp16.safetensors and clip_l.safetensors go in: ComfyUI/models/clip/\n\nae.safetensors goes in: ComfyUI/models/vae/\n\n\nTip: You can set the weight_dtype above to one of the fp8 types if you have memory issues."
|
||||
"If you get an error in any of the nodes above make sure the files are in the correct directories.\n\nSee the top of the examples page for the links : https://comfyanonymous.github.io/ComfyUI_examples/flux/\n\nflux1-dev.safetensors goes in: ComfyUI/models/diffusion_models/\n\nt5xxl_fp16.safetensors and clip_l.safetensors go in: ComfyUI/models/text_encoders/\n\nae.safetensors goes in: ComfyUI/models/vae/\n\n\nTip: You can set the weight_dtype above to one of the fp8 types if you have memory issues."
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
@@ -923,7 +923,7 @@
|
||||
"directory": "text_encoders"
|
||||
},
|
||||
{
|
||||
"name": "flux1-dev-fp8.safetensors",
|
||||
"name": "flux1-dev.safetensors",
|
||||
"url": "https://huggingface.co/black-forest-labs/FLUX.1-dev/resolve/main/flux1-dev.safetensors?download=true",
|
||||
"directory": "diffusion_models"
|
||||
},
|
||||
@@ -932,11 +932,6 @@
|
||||
"url": "https://huggingface.co/Comfy-Org/sigclip_vision_384/resolve/main/sigclip_vision_patch14_384.safetensors?download=true",
|
||||
"directory": "clip_vision"
|
||||
},
|
||||
{
|
||||
"name": "flux1-dev-fp8.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/flux1-dev/resolve/main/flux1-dev-fp8.safetensors?download=true",
|
||||
"directory": "checkpoints"
|
||||
},
|
||||
{
|
||||
"name": "ae.safetensors",
|
||||
"url": "https://huggingface.co/black-forest-labs/FLUX.1-schnell/resolve/main/ae.safetensors?download=true",
|
||||
@@ -944,7 +939,7 @@
|
||||
},
|
||||
{
|
||||
"name": "flux1-redux-dev.safetensors",
|
||||
"url": "https://huggingface.co/black-forest-labs/FLUX.1-Redux-dev/resolve/main/flux1-redux-dev.safetensors",
|
||||
"url": "https://huggingface.co/black-forest-labs/FLUX.1-Redux-dev/resolve/main/flux1-redux-dev.safetensors?download=true",
|
||||
"directory": "style_models"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -481,11 +481,6 @@
|
||||
"url": "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors?download=true",
|
||||
"directory": "checkpoints"
|
||||
},
|
||||
{
|
||||
"name": "sd_xl_refiner_1.0.safetensors",
|
||||
"url": "https://huggingface.co/stabilityai/stable-diffusion-xl-refiner-1.0/resolve/main/sd_xl_refiner_1.0.safetensors?download=true",
|
||||
"directory": "checkpoints"
|
||||
},
|
||||
{
|
||||
"name": "clip_vision_g.safetensors",
|
||||
"url": "https://huggingface.co/comfyanonymous/clip_vision_g/resolve/main/clip_vision_g.safetensors?download=true",
|
||||
|
||||
@@ -485,11 +485,6 @@
|
||||
"url": "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors?download=true",
|
||||
"directory": "checkpoints"
|
||||
},
|
||||
{
|
||||
"name": "sd_xl_refiner_1.0.safetensors",
|
||||
"url": "https://huggingface.co/stabilityai/stable-diffusion-xl-refiner-1.0/resolve/main/sd_xl_refiner_1.0.safetensors?download=true",
|
||||
"directory": "checkpoints"
|
||||
},
|
||||
{
|
||||
"name": "clip_vision_g.safetensors",
|
||||
"url": "https://huggingface.co/comfyanonymous/clip_vision_g/resolve/main/clip_vision_g.safetensors?download=true",
|
||||
|
||||
@@ -1,425 +0,0 @@
|
||||
{
|
||||
"last_node_id": 17,
|
||||
"last_link_id": 23,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 8,
|
||||
"type": "VAEDecode",
|
||||
"pos": [1235.7215576171875, 577.1878662109375],
|
||||
"size": [210, 46],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"localized_name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": 7
|
||||
},
|
||||
{ "name": "vae", "localized_name": "vae", "type": "VAE", "link": 21 }
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"localized_name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [9],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": { "Node name for S&R": "VAEDecode" },
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "LatentUpscale",
|
||||
"pos": [1238, 170],
|
||||
"size": [315, 130],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"localized_name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": 10
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"localized_name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [14]
|
||||
}
|
||||
],
|
||||
"properties": { "Node name for S&R": "LatentUpscale" },
|
||||
"widgets_values": ["nearest-exact", 1152, 1152, "disabled"]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"type": "VAEDecode",
|
||||
"pos": [1961, 125],
|
||||
"size": [210, 46],
|
||||
"flags": {},
|
||||
"order": 10,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"localized_name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": 15
|
||||
},
|
||||
{ "name": "vae", "localized_name": "vae", "type": "VAE", "link": 22 }
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"localized_name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [17],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": { "Node name for S&R": "VAEDecode" },
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [374, 171],
|
||||
"size": [422.84503173828125, 164.31304931640625],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "clip", "localized_name": "clip", "type": "CLIP", "link": 19 }
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"localized_name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [4, 12],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": { "Node name for S&R": "CLIPTextEncode" },
|
||||
"widgets_values": [
|
||||
"masterpiece HDR victorian portrait painting of woman, blonde hair, mountain nature, blue sky\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [377, 381],
|
||||
"size": [425.27801513671875, 180.6060791015625],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "clip", "localized_name": "clip", "type": "CLIP", "link": 20 }
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"localized_name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [6, 13],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": { "Node name for S&R": "CLIPTextEncode" },
|
||||
"widgets_values": ["bad hands, text, watermark\n"]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "EmptyLatentImage",
|
||||
"pos": [435, 600],
|
||||
"size": [315, 106],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"localized_name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [2],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": { "Node name for S&R": "EmptyLatentImage" },
|
||||
"widgets_values": [768, 768, 1]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "KSampler",
|
||||
"pos": [1585, 114],
|
||||
"size": [315, 262],
|
||||
"flags": {},
|
||||
"order": 9,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"localized_name": "model",
|
||||
"type": "MODEL",
|
||||
"link": 23,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"localized_name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": 12,
|
||||
"slot_index": 1
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"localized_name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": 13,
|
||||
"slot_index": 2
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"localized_name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": 14,
|
||||
"slot_index": 3
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"localized_name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [15],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": { "Node name for S&R": "KSampler" },
|
||||
"widgets_values": [
|
||||
469771404043268,
|
||||
"randomize",
|
||||
14,
|
||||
8,
|
||||
"dpmpp_2m",
|
||||
"simple",
|
||||
0.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "SaveImage",
|
||||
"pos": [2203, 123],
|
||||
"size": [407.53717041015625, 468.13226318359375],
|
||||
"flags": {},
|
||||
"order": 11,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"localized_name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 17
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": ["ComfyUI"]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "KSampler",
|
||||
"pos": [845, 172],
|
||||
"size": [315, 262],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"localized_name": "model",
|
||||
"type": "MODEL",
|
||||
"link": 18
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"localized_name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": 4
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"localized_name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": 6
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"localized_name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"localized_name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [7, 10],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": { "Node name for S&R": "KSampler" },
|
||||
"widgets_values": [
|
||||
89848141647836,
|
||||
"randomize",
|
||||
12,
|
||||
8,
|
||||
"dpmpp_sde",
|
||||
"normal",
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [24, 315],
|
||||
"size": [315, 98],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"localized_name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"links": [18, 23],
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"localized_name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"links": [19, 20],
|
||||
"slot_index": 1
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"localized_name": "VAE",
|
||||
"type": "VAE",
|
||||
"links": [21, 22],
|
||||
"slot_index": 2
|
||||
}
|
||||
],
|
||||
"properties": { "Node name for S&R": "CheckpointLoaderSimple" },
|
||||
"widgets_values": ["v2-1_768-ema-pruned.safetensors"]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "SaveImage",
|
||||
"pos": [1495.7215576171875, 576.1878662109375],
|
||||
"size": [232.94032287597656, 282.4336242675781],
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"localized_name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 9
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": ["ComfyUI"]
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"type": "MarkdownNote",
|
||||
"pos": [0, 795],
|
||||
"size": [225, 60],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"🛈 [Learn more about this workflow](https://comfyanonymous.github.io/ComfyUI_examples/upscale_models/)"
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[2, 5, 0, 3, 3, "LATENT"],
|
||||
[4, 6, 0, 3, 1, "CONDITIONING"],
|
||||
[6, 7, 0, 3, 2, "CONDITIONING"],
|
||||
[7, 3, 0, 8, 0, "LATENT"],
|
||||
[9, 8, 0, 9, 0, "IMAGE"],
|
||||
[10, 3, 0, 10, 0, "LATENT"],
|
||||
[12, 6, 0, 11, 1, "CONDITIONING"],
|
||||
[13, 7, 0, 11, 2, "CONDITIONING"],
|
||||
[14, 10, 0, 11, 3, "LATENT"],
|
||||
[15, 11, 0, 13, 0, "LATENT"],
|
||||
[17, 13, 0, 12, 0, "IMAGE"],
|
||||
[18, 16, 0, 3, 0, "MODEL"],
|
||||
[19, 16, 1, 6, 0, "CLIP"],
|
||||
[20, 16, 1, 7, 0, "CLIP"],
|
||||
[21, 16, 2, 8, 1, "VAE"],
|
||||
[22, 16, 2, 13, 1, "VAE"],
|
||||
[23, 16, 0, 11, 0, "MODEL"]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Txt2Img",
|
||||
"bounding": [0, 30, 1211, 708],
|
||||
"color": "#a1309b",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Save Intermediate Image",
|
||||
"bounding": [1230, 495, 516, 196],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Hires Fix",
|
||||
"bounding": [1230, 30, 710, 464],
|
||||
"color": "#b58b2a",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "Save Final Image",
|
||||
"bounding": [1950, 30, 483, 199],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.8037574853834974,
|
||||
"offset": [540.0834501660246, 269.28523360433144]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
:aria-label="$t('menu.showMenu')"
|
||||
aria-live="assertive"
|
||||
@click="exitFocusMode"
|
||||
@contextmenu="showNativeMenu"
|
||||
@contextmenu="showNativeSystemMenu"
|
||||
/>
|
||||
<div v-show="menuSetting !== 'Bottom'" class="window-actions-spacer" />
|
||||
</div>
|
||||
@@ -26,7 +26,7 @@ import { CSSProperties, computed, watchEffect } from 'vue'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { showNativeMenu } from '@/utils/envUtil'
|
||||
import { showNativeSystemMenu } from '@/utils/envUtil'
|
||||
|
||||
const workspaceState = useWorkspaceStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
83
src/components/common/ColorCustomizationSelector.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div
|
||||
class="color-customization-selector-container flex flex-row items-center gap-2"
|
||||
>
|
||||
<SelectButton
|
||||
v-model="selectedColorOption"
|
||||
:options="colorOptionsWithCustom"
|
||||
optionLabel="name"
|
||||
dataKey="value"
|
||||
:allow-empty="false"
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<div
|
||||
v-if="slotProps.option.name !== '_custom'"
|
||||
:style="{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
backgroundColor: slotProps.option.value,
|
||||
borderRadius: '50%'
|
||||
}"
|
||||
></div>
|
||||
<i v-else class="pi pi-palette text-lg"></i>
|
||||
</template>
|
||||
</SelectButton>
|
||||
<ColorPicker
|
||||
v-if="selectedColorOption.name === '_custom'"
|
||||
v-model="customColorValue"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ColorPicker from 'primevue/colorpicker'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const { modelValue, colorOptions } = defineProps<{
|
||||
modelValue: string | null
|
||||
colorOptions: { name: Exclude<string, '_custom'>; value: string }[]
|
||||
}>()
|
||||
|
||||
const customColorOption = { name: '_custom', value: '' }
|
||||
const colorOptionsWithCustom = computed(() => [
|
||||
...colorOptions,
|
||||
customColorOption
|
||||
])
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string | null]
|
||||
}>()
|
||||
|
||||
const selectedColorOption = ref(customColorOption)
|
||||
const customColorValue = ref('')
|
||||
|
||||
// Initialize the component with the provided modelValue
|
||||
onMounted(() => {
|
||||
if (modelValue) {
|
||||
const predefinedColor = colorOptions.find((opt) => opt.value === modelValue)
|
||||
if (predefinedColor) {
|
||||
selectedColorOption.value = predefinedColor
|
||||
} else {
|
||||
selectedColorOption.value = customColorOption
|
||||
customColorValue.value = modelValue.replace('#', '')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for changes in selection and emit updates
|
||||
watch(selectedColorOption, (newOption, oldOption) => {
|
||||
if (newOption.name === '_custom') {
|
||||
// Inherit the color from previous selection
|
||||
customColorValue.value = oldOption.value.replace('#', '')
|
||||
} else {
|
||||
emit('update:modelValue', newOption.value)
|
||||
}
|
||||
})
|
||||
|
||||
watch(customColorValue, (newValue) => {
|
||||
if (selectedColorOption.value.name === '_custom') {
|
||||
emit('update:modelValue', newValue ? `#${newValue}` : null)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -20,37 +20,10 @@
|
||||
<Divider />
|
||||
<div class="field color-field">
|
||||
<label for="color">{{ $t('g.color') }}</label>
|
||||
<div class="color-picker-container">
|
||||
<SelectButton
|
||||
v-model="selectedColor"
|
||||
:options="colorOptions"
|
||||
optionLabel="name"
|
||||
dataKey="value"
|
||||
:allow-empty="false"
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<div
|
||||
v-if="slotProps.option.value !== 'custom'"
|
||||
:style="{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
backgroundColor: slotProps.option.value,
|
||||
borderRadius: '50%'
|
||||
}"
|
||||
></div>
|
||||
<i
|
||||
v-else
|
||||
class="pi pi-palette"
|
||||
:style="{ fontSize: '1.2rem' }"
|
||||
v-tooltip="$t('color.custom')"
|
||||
></i>
|
||||
</template>
|
||||
</SelectButton>
|
||||
<ColorPicker
|
||||
v-if="selectedColor.value === 'custom'"
|
||||
v-model="customColor"
|
||||
/>
|
||||
</div>
|
||||
<ColorCustomizationSelector
|
||||
v-model="finalColor"
|
||||
:color-options="colorOptions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
@@ -72,13 +45,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import ColorPicker from 'primevue/colorpicker'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import Divider from 'primevue/divider'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ColorCustomizationSelector from '@/components/common/ColorCustomizationSelector.vue'
|
||||
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -118,29 +91,24 @@ const colorOptions = [
|
||||
{ name: t('color.green'), value: '#28a745' },
|
||||
{ name: t('color.red'), value: '#dc3545' },
|
||||
{ name: t('color.pink'), value: '#e83e8c' },
|
||||
{ name: t('color.yellow'), value: '#ffc107' },
|
||||
{ name: t('color.custom'), value: 'custom' }
|
||||
{ name: t('color.yellow'), value: '#ffc107' }
|
||||
]
|
||||
|
||||
const defaultIcon = iconOptions.find(
|
||||
(option) => option.value === nodeBookmarkStore.defaultBookmarkIcon
|
||||
)
|
||||
const defaultColor = colorOptions.find(
|
||||
(option) => option.value === nodeBookmarkStore.defaultBookmarkColor
|
||||
)
|
||||
|
||||
const selectedIcon = ref<{ name: string; value: string }>(defaultIcon)
|
||||
const selectedColor = ref<{ name: string; value: string }>(defaultColor)
|
||||
const finalColor = computed(() =>
|
||||
selectedColor.value.value === 'custom'
|
||||
? `#${customColor.value}`
|
||||
: selectedColor.value.value
|
||||
const finalColor = ref(
|
||||
props.initialColor || nodeBookmarkStore.defaultBookmarkColor
|
||||
)
|
||||
|
||||
const customColor = ref('000000')
|
||||
|
||||
const closeDialog = () => {
|
||||
visible.value = false
|
||||
const resetCustomization = () => {
|
||||
selectedIcon.value =
|
||||
iconOptions.find((option) => option.value === props.initialIcon) ||
|
||||
defaultIcon
|
||||
finalColor.value =
|
||||
props.initialColor || nodeBookmarkStore.defaultBookmarkColor
|
||||
}
|
||||
|
||||
const confirmCustomization = () => {
|
||||
@@ -148,21 +116,8 @@ const confirmCustomization = () => {
|
||||
closeDialog()
|
||||
}
|
||||
|
||||
const resetCustomization = () => {
|
||||
selectedIcon.value =
|
||||
iconOptions.find((option) => option.value === props.initialIcon) ||
|
||||
defaultIcon
|
||||
const colorOption = colorOptions.find(
|
||||
(option) => option.value === props.initialColor
|
||||
)
|
||||
if (!props.initialColor) {
|
||||
selectedColor.value = defaultColor
|
||||
} else if (!colorOption) {
|
||||
customColor.value = props.initialColor.replace('#', '')
|
||||
selectedColor.value = { name: t('color.custom'), value: 'custom' }
|
||||
} else {
|
||||
selectedColor.value = colorOption
|
||||
}
|
||||
const closeDialog = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -190,10 +145,4 @@ watch(
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.color-picker-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ColorPicker from 'primevue/colorpicker'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { createApp, nextTick } from 'vue'
|
||||
|
||||
import ColorCustomizationSelector from '../ColorCustomizationSelector.vue'
|
||||
|
||||
describe('ColorCustomizationSelector', () => {
|
||||
const colorOptions = [
|
||||
{ name: 'Blue', value: '#0d6efd' },
|
||||
{ name: 'Green', value: '#28a745' }
|
||||
]
|
||||
|
||||
beforeEach(() => {
|
||||
// Setup PrimeVue
|
||||
const app = createApp({})
|
||||
app.use(PrimeVue)
|
||||
})
|
||||
|
||||
const mountComponent = (props = {}) => {
|
||||
return mount(ColorCustomizationSelector, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: { SelectButton, ColorPicker }
|
||||
},
|
||||
props: {
|
||||
modelValue: null,
|
||||
colorOptions,
|
||||
...props
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
it('renders predefined color options and custom option', () => {
|
||||
const wrapper = mountComponent()
|
||||
const selectButton = wrapper.findComponent(SelectButton)
|
||||
|
||||
expect(selectButton.props('options')).toHaveLength(colorOptions.length + 1)
|
||||
expect(selectButton.props('options')?.at(-1)?.name).toBe('_custom')
|
||||
})
|
||||
|
||||
it('initializes with predefined color when provided', async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: '#0d6efd'
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
const selectButton = wrapper.findComponent(SelectButton)
|
||||
expect(selectButton.props('modelValue')).toEqual({
|
||||
name: 'Blue',
|
||||
value: '#0d6efd'
|
||||
})
|
||||
})
|
||||
|
||||
it('initializes with custom color when non-predefined color provided', async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: '#123456'
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
const selectButton = wrapper.findComponent(SelectButton)
|
||||
const colorPicker = wrapper.findComponent(ColorPicker)
|
||||
|
||||
expect(selectButton.props('modelValue').name).toBe('_custom')
|
||||
expect(colorPicker.props('modelValue')).toBe('123456')
|
||||
})
|
||||
|
||||
it('shows color picker when custom option is selected', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const selectButton = wrapper.findComponent(SelectButton)
|
||||
|
||||
// Select custom option
|
||||
await selectButton.setValue({ name: '_custom', value: '' })
|
||||
|
||||
expect(wrapper.findComponent(ColorPicker).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('emits update when predefined color is selected', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const selectButton = wrapper.findComponent(SelectButton)
|
||||
|
||||
await selectButton.setValue(colorOptions[0])
|
||||
|
||||
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['#0d6efd'])
|
||||
})
|
||||
|
||||
it('emits update when custom color is changed', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const selectButton = wrapper.findComponent(SelectButton)
|
||||
|
||||
// Select custom option
|
||||
await selectButton.setValue({ name: '_custom', value: '' })
|
||||
|
||||
// Change custom color
|
||||
const colorPicker = wrapper.findComponent(ColorPicker)
|
||||
await colorPicker.setValue('ff0000')
|
||||
|
||||
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['#ff0000'])
|
||||
})
|
||||
|
||||
it('inherits color from previous selection when switching to custom', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const selectButton = wrapper.findComponent(SelectButton)
|
||||
|
||||
// First select a predefined color
|
||||
await selectButton.setValue(colorOptions[0])
|
||||
|
||||
// Then switch to custom
|
||||
await selectButton.setValue({ name: '_custom', value: '' })
|
||||
|
||||
const colorPicker = wrapper.findComponent(ColorPicker)
|
||||
expect(colorPicker.props('modelValue')).toBe('0d6efd')
|
||||
})
|
||||
|
||||
it('handles null modelValue correctly', async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: null
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
const selectButton = wrapper.findComponent(SelectButton)
|
||||
expect(selectButton.props('modelValue')).toEqual({
|
||||
name: '_custom',
|
||||
value: ''
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -51,7 +51,8 @@ const allowedSources = [
|
||||
const allowedSuffixes = ['.safetensors', '.sft']
|
||||
// Models that fail above conditions but are still allowed
|
||||
const whiteListedUrls = new Set([
|
||||
'https://huggingface.co/stabilityai/stable-zero123/resolve/main/stable_zero123.ckpt'
|
||||
'https://huggingface.co/stabilityai/stable-zero123/resolve/main/stable_zero123.ckpt',
|
||||
'https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_depth_sd14v1.pth?download=true'
|
||||
])
|
||||
|
||||
interface ModelInfo {
|
||||
|
||||
@@ -28,9 +28,8 @@
|
||||
class="w-full h-full touch-none"
|
||||
/>
|
||||
<NodeSearchboxPopover />
|
||||
<SelectionOverlay>
|
||||
<!-- Placeholder for selection overlay testing. -->
|
||||
<!-- <div class="w-full h-full bg-red-500"></div> -->
|
||||
<SelectionOverlay v-if="selectionToolboxEnabled">
|
||||
<SelectionToolbox />
|
||||
</SelectionOverlay>
|
||||
<NodeTooltip v-if="tooltipEnabled" />
|
||||
<NodeBadge />
|
||||
@@ -45,10 +44,12 @@ import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
|
||||
import NodeBadge from '@/components/graph/NodeBadge.vue'
|
||||
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
|
||||
import SelectionOverlay from '@/components/graph/SelectionOverlay.vue'
|
||||
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
|
||||
import TitleEditor from '@/components/graph/TitleEditor.vue'
|
||||
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
|
||||
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
|
||||
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { useCanvasDrop } from '@/composables/useCanvasDrop'
|
||||
import { useContextMenuTranslation } from '@/composables/useContextMenuTranslation'
|
||||
import { useCopy } from '@/composables/useCopy'
|
||||
@@ -87,6 +88,9 @@ const canvasMenuEnabled = computed(() =>
|
||||
settingStore.get('Comfy.Graph.CanvasMenu')
|
||||
)
|
||||
const tooltipEnabled = computed(() => settingStore.get('Comfy.EnableTooltips'))
|
||||
const selectionToolboxEnabled = computed(() =>
|
||||
settingStore.get('Comfy.Canvas.SelectionToolbox')
|
||||
)
|
||||
|
||||
watchEffect(() => {
|
||||
nodeDefStore.showDeprecated = settingStore.get('Comfy.Node.ShowDeprecated')
|
||||
@@ -192,6 +196,11 @@ onMounted(async () => {
|
||||
|
||||
comfyAppReady.value = true
|
||||
|
||||
comfyApp.canvas.onSelectionChange = useChainCallback(
|
||||
comfyApp.canvas.onSelectionChange,
|
||||
() => canvasStore.updateSelectedItems()
|
||||
)
|
||||
|
||||
// Load color palette
|
||||
colorPaletteStore.customPalettes = settingStore.get(
|
||||
'Comfy.CustomColorPalettes'
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<!-- This component is used to bound the selected items on the canvas. -->
|
||||
<template>
|
||||
<div
|
||||
class="selection-overlay-container pointer-events-none"
|
||||
class="selection-overlay-container pointer-events-none z-40"
|
||||
:class="{
|
||||
'show-border': showBorder
|
||||
}"
|
||||
:style="style"
|
||||
v-show="visible"
|
||||
>
|
||||
@@ -22,9 +25,12 @@ const canvasStore = useCanvasStore()
|
||||
const { style, updatePosition } = useAbsolutePosition()
|
||||
|
||||
const visible = ref(false)
|
||||
const showBorder = ref(false)
|
||||
|
||||
const positionSelectionOverlay = (canvas: LGraphCanvas) => {
|
||||
const selectedItems = canvas.selectedItems
|
||||
showBorder.value = selectedItems.size > 1
|
||||
|
||||
if (!selectedItems.size) {
|
||||
visible.value = false
|
||||
return
|
||||
@@ -82,4 +88,8 @@ watch(
|
||||
.selection-overlay-container > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.show-border {
|
||||
@apply border-dashed rounded-md border-2 border-[var(--border-color)];
|
||||
}
|
||||
</style>
|
||||
|
||||
66
src/components/graph/SelectionToolbox.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<Panel
|
||||
class="selection-toolbox absolute left-1/2 rounded-lg"
|
||||
:pt="{
|
||||
header: 'hidden',
|
||||
content: 'p-0 flex flex-row'
|
||||
}"
|
||||
>
|
||||
<Button
|
||||
v-if="nodeSelected"
|
||||
severity="secondary"
|
||||
text
|
||||
@click="
|
||||
() => commandStore.execute('Comfy.Canvas.ToggleSelectedNodes.Bypass')
|
||||
"
|
||||
data-testid="bypass-button"
|
||||
>
|
||||
<template #icon>
|
||||
<i-game-icons:detour />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
severity="secondary"
|
||||
text
|
||||
icon="pi pi-thumbtack"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.ToggleSelected.Pin')"
|
||||
/>
|
||||
<Button
|
||||
severity="danger"
|
||||
text
|
||||
icon="pi pi-trash"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.DeleteSelectedItems')"
|
||||
/>
|
||||
<Button
|
||||
v-if="isRefreshable"
|
||||
severity="info"
|
||||
text
|
||||
icon="pi pi-refresh"
|
||||
@click="refreshSelected"
|
||||
/>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Panel from 'primevue/panel'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useRefreshableSelection } from '@/composables/useRefreshableSelection'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { isLGraphNode } from '@/utils/litegraphUtil'
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const { isRefreshable, refreshSelected } = useRefreshableSelection()
|
||||
const nodeSelected = computed(() =>
|
||||
canvasStore.selectedItems.some(isLGraphNode)
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selection-toolbox {
|
||||
transform: translateX(-50%) translateY(-120%);
|
||||
}
|
||||
</style>
|
||||
@@ -21,7 +21,7 @@
|
||||
v-tooltip="{ value: $t('menu.hideMenu'), showDelay: 300 }"
|
||||
:aria-label="$t('menu.hideMenu')"
|
||||
@click="workspaceState.focusMode = true"
|
||||
@contextmenu="showNativeMenu"
|
||||
@contextmenu="showNativeSystemMenu"
|
||||
/>
|
||||
<div
|
||||
v-show="menuSetting !== 'Bottom'"
|
||||
@@ -52,7 +52,7 @@ import {
|
||||
electronAPI,
|
||||
isElectron,
|
||||
isNativeWindow,
|
||||
showNativeMenu
|
||||
showNativeSystemMenu
|
||||
} from '@/utils/envUtil'
|
||||
|
||||
const workspaceState = useWorkspaceStore()
|
||||
|
||||
@@ -366,6 +366,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
versionAdded: '1.3.11',
|
||||
function: () => {
|
||||
toggleSelectedNodesMode(LGraphEventMode.NEVER)
|
||||
app.canvas.setDirty(true, true)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -375,6 +376,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
versionAdded: '1.3.11',
|
||||
function: () => {
|
||||
toggleSelectedNodesMode(LGraphEventMode.BYPASS)
|
||||
app.canvas.setDirty(true, true)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -386,6 +388,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
getSelectedNodes().forEach((node) => {
|
||||
node.pin(!node.pinned)
|
||||
})
|
||||
app.canvas.setDirty(true, true)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -399,6 +402,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
item.pin(!item.pinned)
|
||||
}
|
||||
}
|
||||
app.canvas.setDirty(true, true)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -410,6 +414,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
getSelectedNodes().forEach((node) => {
|
||||
node.collapse()
|
||||
})
|
||||
app.canvas.setDirty(true, true)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -566,6 +571,16 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
function: () => {
|
||||
window.open('https://forum.comfy.org/', '_blank')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.DeleteSelectedItems',
|
||||
icon: 'pi pi-trash',
|
||||
label: 'Delete Selected Items',
|
||||
versionAdded: '1.10.5',
|
||||
function: () => {
|
||||
app.canvas.deleteSelected()
|
||||
app.canvas.setDirty(true, true)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
45
src/composables/useNodeFileInput.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
interface FileInputOptions {
|
||||
accept?: string
|
||||
allow_batch?: boolean
|
||||
fileFilter?: (file: File) => boolean
|
||||
onSelect: (files: File[]) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a file input for a node.
|
||||
*/
|
||||
export function useNodeFileInput(options: FileInputOptions) {
|
||||
const {
|
||||
accept,
|
||||
allow_batch = false,
|
||||
fileFilter = () => true,
|
||||
onSelect
|
||||
} = options
|
||||
|
||||
const fileInput = document.createElement('input')
|
||||
fileInput.type = 'file'
|
||||
fileInput.accept = accept ?? '*'
|
||||
fileInput.multiple = allow_batch
|
||||
fileInput.style.visibility = 'hidden'
|
||||
|
||||
fileInput.onchange = () => {
|
||||
if (fileInput.files?.length) {
|
||||
const files = Array.from(fileInput.files).filter(fileFilter)
|
||||
if (files.length) onSelect(files)
|
||||
}
|
||||
}
|
||||
|
||||
document.body.append(fileInput)
|
||||
|
||||
/**
|
||||
* Shows the system file picker dialog for selecting files.
|
||||
*/
|
||||
function openFileSelection() {
|
||||
fileInput.click()
|
||||
}
|
||||
|
||||
return {
|
||||
fileInput,
|
||||
openFileSelection
|
||||
}
|
||||
}
|
||||
@@ -2,25 +2,16 @@ import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
|
||||
interface NodeImageOptions {
|
||||
allowBatch?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches a preview image to a node.
|
||||
*/
|
||||
export const useNodeImage = (node: LGraphNode, options: NodeImageOptions) => {
|
||||
const { allowBatch = false } = options
|
||||
export const useNodeImage = (node: LGraphNode) => {
|
||||
const nodeOutputStore = useNodeOutputStore()
|
||||
|
||||
/** Displays output image(s) on the node. */
|
||||
function showImage(output: string | string[]) {
|
||||
if (!output) return
|
||||
if (allowBatch || typeof output === 'string') {
|
||||
nodeOutputStore.setNodeOutputs(node, output)
|
||||
} else {
|
||||
nodeOutputStore.setNodeOutputs(node, output[0])
|
||||
}
|
||||
nodeOutputStore.setNodeOutputs(node, output)
|
||||
node.setSizeForImage?.()
|
||||
node.graph?.setDirtyCanvas(true)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import { useNodeDragAndDrop } from '@/composables/useNodeDragAndDrop'
|
||||
import { useNodeFileInput } from '@/composables/useNodeFileInput'
|
||||
import { useNodePaste } from '@/composables/useNodePaste'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
import { useNodeDragAndDrop } from './useNodeDragAndDrop'
|
||||
import { useNodePaste } from './useNodePaste'
|
||||
|
||||
const ACCEPTED_IMAGE_TYPES = 'image/jpeg,image/png,image/webp'
|
||||
const PASTED_IMAGE_EXPIRY_MS = 2000
|
||||
|
||||
const createFileInput = () => {
|
||||
const fileInput = document.createElement('input')
|
||||
fileInput.type = 'file'
|
||||
fileInput.accept = ACCEPTED_IMAGE_TYPES
|
||||
return fileInput
|
||||
}
|
||||
|
||||
const uploadFile = async (file: File, isPasted: boolean) => {
|
||||
const body = new FormData()
|
||||
body.append('image', file)
|
||||
@@ -38,13 +31,17 @@ const uploadFile = async (file: File, isPasted: boolean) => {
|
||||
interface ImageUploadOptions {
|
||||
fileFilter?: (file: File) => boolean
|
||||
onUploadComplete: (paths: string[]) => void
|
||||
allow_batch?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds image upload to a node via drag & drop, paste, and file input.
|
||||
*/
|
||||
export const useNodeImageUpload = (
|
||||
node: LGraphNode,
|
||||
options: ImageUploadOptions
|
||||
) => {
|
||||
const { fileFilter = () => true, onUploadComplete } = options
|
||||
const { fileFilter, onUploadComplete, allow_batch } = options
|
||||
|
||||
const isPastedFile = (file: File): boolean =>
|
||||
file.name === 'image.png' &&
|
||||
@@ -60,48 +57,33 @@ export const useNodeImageUpload = (
|
||||
}
|
||||
}
|
||||
|
||||
const handleUploadBatch = async (files: File[]) => {
|
||||
const paths = await Promise.all(files.map(handleUpload))
|
||||
const validPaths = paths.filter((p): p is string => !!p)
|
||||
if (validPaths.length) onUploadComplete(validPaths)
|
||||
return validPaths
|
||||
}
|
||||
|
||||
// Handle drag & drop
|
||||
useNodeDragAndDrop(node, {
|
||||
fileFilter,
|
||||
onDrop: async (files) => {
|
||||
const paths = await Promise.all(files.map(handleUpload))
|
||||
const validPaths = paths.filter((p): p is string => !!p)
|
||||
if (validPaths.length) {
|
||||
onUploadComplete(validPaths)
|
||||
}
|
||||
return validPaths
|
||||
}
|
||||
onDrop: handleUploadBatch
|
||||
})
|
||||
|
||||
// Handle paste
|
||||
useNodePaste(node, {
|
||||
fileFilter,
|
||||
onPaste: async (file) => {
|
||||
const path = await handleUpload(file)
|
||||
if (path) {
|
||||
onUploadComplete([path])
|
||||
}
|
||||
return path
|
||||
}
|
||||
allow_batch,
|
||||
onPaste: handleUploadBatch
|
||||
})
|
||||
|
||||
// Handle file input
|
||||
const fileInput = createFileInput()
|
||||
fileInput.onchange = async () => {
|
||||
if (fileInput.files?.length) {
|
||||
const paths = await Promise.all(
|
||||
Array.from(fileInput.files).filter(fileFilter).map(handleUpload)
|
||||
)
|
||||
const validPaths = paths.filter((p): p is string => !!p)
|
||||
if (validPaths.length) {
|
||||
onUploadComplete(validPaths)
|
||||
}
|
||||
}
|
||||
}
|
||||
document.body.append(fileInput)
|
||||
const { openFileSelection } = useNodeFileInput({
|
||||
fileFilter,
|
||||
allow_batch,
|
||||
accept: ACCEPTED_IMAGE_TYPES,
|
||||
onSelect: handleUploadBatch
|
||||
})
|
||||
|
||||
return {
|
||||
fileInput,
|
||||
handleUpload
|
||||
}
|
||||
return { openFileSelection, handleUpload }
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
type PasteHandler<T> = (file: File) => Promise<T>
|
||||
type PasteHandler<T> = (files: File[]) => Promise<T>
|
||||
|
||||
interface NodePasteOptions<T> {
|
||||
onPaste: PasteHandler<T>
|
||||
fileFilter?: (file: File) => boolean
|
||||
allow_batch?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -14,12 +15,15 @@ export const useNodePaste = <T>(
|
||||
node: LGraphNode,
|
||||
options: NodePasteOptions<T>
|
||||
) => {
|
||||
const { onPaste, fileFilter = () => true } = options
|
||||
const { onPaste, fileFilter = () => true, allow_batch = false } = options
|
||||
|
||||
node.pasteFile = function (file: File) {
|
||||
if (!fileFilter(file)) return false
|
||||
node.pasteFiles = function (files: File[]) {
|
||||
const filteredFiles = Array.from(files).filter(fileFilter)
|
||||
if (!filteredFiles.length) return false
|
||||
|
||||
onPaste(file).then((result) => {
|
||||
const paste = allow_batch ? filteredFiles : filteredFiles.slice(0, 1)
|
||||
|
||||
onPaste(paste).then((result) => {
|
||||
if (!result) return
|
||||
})
|
||||
return true
|
||||
|
||||
@@ -28,7 +28,7 @@ export const usePaste = () => {
|
||||
// Did you mean 'Clipboard'?ts(2551)
|
||||
// TODO: Not sure what the code wants to do.
|
||||
let data = e.clipboardData || window.clipboardData
|
||||
const items = data.items
|
||||
const items: DataTransferItemList = data.items
|
||||
|
||||
// Look for image paste data
|
||||
for (const item of items) {
|
||||
@@ -54,7 +54,13 @@ export const usePaste = () => {
|
||||
graph.change()
|
||||
}
|
||||
const blob = item.getAsFile()
|
||||
imageNode?.pasteFile?.(blob)
|
||||
if (blob) imageNode?.pasteFile?.(blob)
|
||||
|
||||
imageNode?.pasteFiles?.(
|
||||
Array.from(items)
|
||||
.map((i) => i.getAsFile())
|
||||
.filter((f) => f !== null)
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
57
src/composables/useRefreshableSelection.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { IWidget } from '@comfyorg/litegraph'
|
||||
import { computed, ref, watchEffect } from 'vue'
|
||||
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { isLGraphNode } from '@/utils/litegraphUtil'
|
||||
|
||||
interface RefreshableItem {
|
||||
refresh: () => Promise<void> | void
|
||||
}
|
||||
|
||||
type RefreshableWidget = IWidget & RefreshableItem
|
||||
|
||||
const isRefreshableWidget = (widget: IWidget): widget is RefreshableWidget =>
|
||||
'refresh' in widget && typeof widget.refresh === 'function'
|
||||
|
||||
/**
|
||||
* Tracks selected nodes and their refreshable widgets
|
||||
*/
|
||||
export const useRefreshableSelection = () => {
|
||||
const graphStore = useCanvasStore()
|
||||
const commandStore = useCommandStore()
|
||||
const selectedNodes = ref<LGraphNode[]>([])
|
||||
const isAllNodesSelected = ref(false)
|
||||
|
||||
watchEffect(() => {
|
||||
selectedNodes.value = graphStore.selectedItems.filter(isLGraphNode)
|
||||
isAllNodesSelected.value =
|
||||
graphStore.canvas?.graph?.nodes?.every((node) => !!node.selected) ?? false
|
||||
})
|
||||
|
||||
const refreshableWidgets = computed(() =>
|
||||
selectedNodes.value.flatMap(
|
||||
(node) => node.widgets?.filter(isRefreshableWidget) ?? []
|
||||
)
|
||||
)
|
||||
|
||||
const isRefreshable = computed(
|
||||
() => refreshableWidgets.value.length > 0 || isAllNodesSelected.value
|
||||
)
|
||||
|
||||
async function refreshSelected() {
|
||||
if (!isRefreshable.value) return
|
||||
|
||||
if (isAllNodesSelected.value) {
|
||||
commandStore.execute('Comfy.RefreshNodeDefinitions')
|
||||
} else {
|
||||
await Promise.all(refreshableWidgets.value.map((item) => item.refresh()))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isRefreshable,
|
||||
refreshSelected
|
||||
}
|
||||
}
|
||||
31
src/composables/useValueTransform.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Creates a getter/setter pair that transforms values on access if they have changed.
|
||||
* Does not observe deep changes.
|
||||
*
|
||||
* @example
|
||||
* const { get, set } = useValueTransform<ResultItem[], string[]>(
|
||||
* items => items.map(formatPath)
|
||||
* )
|
||||
*
|
||||
* Object.defineProperty(obj, 'value', { get, set })
|
||||
*/
|
||||
export function useValueTransform<Internal, External>(
|
||||
transform: (value: Internal) => External,
|
||||
initialValue: Internal
|
||||
) {
|
||||
let internalValue: Internal = initialValue
|
||||
let cachedValue: External = transform(initialValue)
|
||||
let isChanged = false
|
||||
|
||||
return {
|
||||
get: () => {
|
||||
if (!isChanged) return cachedValue
|
||||
cachedValue = transform(internalValue)
|
||||
return cachedValue
|
||||
},
|
||||
set: (value: Internal) => {
|
||||
isChanged = true
|
||||
internalValue = value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,25 +3,23 @@ import { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
import { useNodeImage } from '@/composables/useNodeImage'
|
||||
import { useNodeImageUpload } from '@/composables/useNodeImageUpload'
|
||||
import { useValueTransform } from '@/composables/useValueTransform'
|
||||
import type { ComfyWidgetConstructor } from '@/scripts/widgets'
|
||||
import type { ComfyApp } from '@/types'
|
||||
import type { InputSpec, ResultItem } from '@/types/apiTypes'
|
||||
import { createAnnotatedPath } from '@/utils/formatUtil'
|
||||
import { addToComboValues } from '@/utils/litegraphUtil'
|
||||
|
||||
type InternalFile = string | ResultItem
|
||||
type InternalValue = InternalFile | InternalFile[]
|
||||
type ExposedValue = string | string[]
|
||||
|
||||
const isImageFile = (file: File) => file.type.startsWith('image/')
|
||||
|
||||
const findFileComboWidget = (node: LGraphNode, inputData: InputSpec) =>
|
||||
node.widgets?.find(
|
||||
(w) => w.name === (inputData[1]?.widget ?? 'image') && w.type === 'combo'
|
||||
) as IComboWidget & { value: string }
|
||||
|
||||
const addToComboValues = (widget: IComboWidget, path: string) => {
|
||||
if (!widget.options) widget.options = { values: [] }
|
||||
if (!widget.options.values) widget.options.values = []
|
||||
if (!widget.options.values.includes(path)) {
|
||||
widget.options.values.push(path)
|
||||
const findFileComboWidget = (node: LGraphNode, inputName: string) =>
|
||||
node.widgets!.find((w) => w.name === inputName) as IComboWidget & {
|
||||
value: ExposedValue
|
||||
}
|
||||
}
|
||||
|
||||
export const useImageUploadWidget = () => {
|
||||
const widgetConstructor: ComfyWidgetConstructor = (
|
||||
@@ -30,43 +28,46 @@ export const useImageUploadWidget = () => {
|
||||
inputData: InputSpec,
|
||||
app: ComfyApp
|
||||
) => {
|
||||
// TODO: specify upload widget via input spec rather than input name
|
||||
const fileComboWidget = findFileComboWidget(node, inputData)
|
||||
const { allow_batch, image_folder = 'input' } = inputData[1] ?? {}
|
||||
const inputOptions = inputData[1] ?? {}
|
||||
const { imageInputName, allow_batch, image_folder = 'input' } = inputOptions
|
||||
|
||||
const { showImage } = useNodeImage(node)
|
||||
|
||||
const fileComboWidget = findFileComboWidget(node, imageInputName)
|
||||
const initialFile = `${fileComboWidget.value}`
|
||||
const { showImage } = useNodeImage(node, { allowBatch: allow_batch })
|
||||
const formatPath = (value: InternalFile) =>
|
||||
createAnnotatedPath(value, { rootFolder: image_folder })
|
||||
|
||||
let internalValue: string | ResultItem = initialFile
|
||||
const transform = (internalValue: InternalValue): ExposedValue => {
|
||||
if (!internalValue) return initialFile
|
||||
if (Array.isArray(internalValue))
|
||||
return allow_batch
|
||||
? internalValue.map(formatPath)
|
||||
: formatPath(internalValue[0])
|
||||
return formatPath(internalValue)
|
||||
}
|
||||
|
||||
// Setup getter/setter that transforms from `ResultItem` to string and formats paths
|
||||
Object.defineProperty(fileComboWidget, 'value', {
|
||||
set: function (value: string | ResultItem) {
|
||||
internalValue = value
|
||||
},
|
||||
get: function () {
|
||||
if (!internalValue) return initialFile
|
||||
if (typeof internalValue === 'string')
|
||||
return createAnnotatedPath(internalValue, {
|
||||
rootFolder: image_folder
|
||||
})
|
||||
if (!internalValue.filename) return initialFile
|
||||
return createAnnotatedPath(internalValue)
|
||||
}
|
||||
})
|
||||
Object.defineProperty(
|
||||
fileComboWidget,
|
||||
'value',
|
||||
useValueTransform(transform, initialFile)
|
||||
)
|
||||
|
||||
// Setup file upload handling
|
||||
const { fileInput } = useNodeImageUpload(node, {
|
||||
const { openFileSelection } = useNodeImageUpload(node, {
|
||||
allow_batch,
|
||||
fileFilter: isImageFile,
|
||||
onUploadComplete: (output) => {
|
||||
output.forEach((path) => addToComboValues(fileComboWidget, path))
|
||||
fileComboWidget.value = output[0]
|
||||
// @ts-expect-error litegraph combo value type does not support arrays yet
|
||||
fileComboWidget.value = output
|
||||
fileComboWidget.callback?.(output)
|
||||
}
|
||||
})
|
||||
|
||||
// Create the button widget for selecting the files
|
||||
const uploadWidget = node.addWidget('button', inputName, 'image', () =>
|
||||
fileInput.click()
|
||||
openFileSelection()
|
||||
)
|
||||
uploadWidget.label = 'choose file to upload'
|
||||
// @ts-expect-error serialize is not typed
|
||||
|
||||
@@ -78,6 +78,7 @@ export function useRemoteWidget<
|
||||
const isPermanent = refresh <= 0
|
||||
const cacheKey = createCacheKey(config)
|
||||
let isLoaded = false
|
||||
let refreshQueued = false
|
||||
|
||||
const setSuccess = (entry: CacheEntry<T>, data: T) => {
|
||||
entry.retryCount = 0
|
||||
@@ -183,12 +184,15 @@ export function useRemoteWidget<
|
||||
/**
|
||||
* Getter of the remote property of the widget (e.g., options.values, value, etc.).
|
||||
* Starts the fetch process then returns the cached value immediately.
|
||||
* @param onFulfilled - Optional callback to be called when the fetch is resolved.
|
||||
* @returns the most recent value of the widget.
|
||||
*/
|
||||
function getValue(onFulfilled?: () => void) {
|
||||
fetchValue().then((data) => {
|
||||
if (isFirstLoad()) onFirstLoad(data)
|
||||
if (refreshQueued && data !== defaultValue) {
|
||||
onRefresh()
|
||||
refreshQueued = false
|
||||
}
|
||||
onFulfilled?.()
|
||||
})
|
||||
return getCachedValue() ?? defaultValue
|
||||
@@ -197,22 +201,23 @@ export function useRemoteWidget<
|
||||
/**
|
||||
* Force the widget to refresh its value
|
||||
*/
|
||||
function refreshValue() {
|
||||
widget.refresh = function () {
|
||||
refreshQueued = true
|
||||
clearCachedValue()
|
||||
getValue(onRefresh)
|
||||
getValue()
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a refresh button to the node that, when clicked, will force the widget to refresh
|
||||
*/
|
||||
function addRefreshButton() {
|
||||
node.addWidget('button', 'refresh', 'refresh', refreshValue)
|
||||
node.addWidget('button', 'refresh', 'refresh', widget.refresh)
|
||||
}
|
||||
|
||||
return {
|
||||
getCachedValue,
|
||||
getValue,
|
||||
refreshValue,
|
||||
refreshValue: widget.refresh,
|
||||
addRefreshButton,
|
||||
getCacheEntry: () => dataCache.get(cacheKey),
|
||||
|
||||
|
||||
@@ -747,5 +747,13 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
defaultValue: 0.6,
|
||||
versionAdded: '1.9.1'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.SelectionToolbox',
|
||||
category: ['LiteGraph', 'Canvas', 'SelectionToolbox'],
|
||||
name: 'Show selection toolbox',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.10.5'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -165,7 +165,7 @@ export const CORE_TEMPLATES = [
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'png',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/upscale_models/'
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/2_pass_txt2img/'
|
||||
},
|
||||
{
|
||||
name: 'esrgan_example',
|
||||
@@ -174,13 +174,6 @@ export const CORE_TEMPLATES = [
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/upscale_models/'
|
||||
},
|
||||
{
|
||||
name: 'hiresfix_latent_workflow',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'png',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/2_pass_txt2img/'
|
||||
},
|
||||
{
|
||||
name: 'hiresfix_esrgan_workflow',
|
||||
mediaType: 'image',
|
||||
|
||||
@@ -27,7 +27,7 @@ export const DESKTOP_MAINTENANCE_TASKS: Readonly<MaintenanceTask>[] = [
|
||||
},
|
||||
{
|
||||
id: 'git',
|
||||
headerImg: '/assets/images/Git-Logo-White.svg',
|
||||
headerImg: 'assets/images/Git-Logo-White.svg',
|
||||
execute: () => openUrl('https://git-scm.com/downloads/'),
|
||||
name: 'Download git',
|
||||
shortDescription: 'Open the git download page.',
|
||||
|
||||
@@ -53,6 +53,7 @@ class Load3d {
|
||||
targetWidth: number = 1024
|
||||
targetHeight: number = 1024
|
||||
showPreview: boolean = true
|
||||
previewWidth: number = 120
|
||||
node: LGraphNode = {} as LGraphNode
|
||||
private listeners: { [key: string]: Function[] } = {}
|
||||
|
||||
@@ -197,19 +198,61 @@ class Load3d {
|
||||
|
||||
this.previewContainer = document.createElement('div')
|
||||
this.previewContainer.style.cssText = `
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
display: block;
|
||||
`
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
display: block;
|
||||
transition: border-color 0.1s ease;
|
||||
`
|
||||
this.previewContainer.appendChild(this.previewRenderer.domElement)
|
||||
|
||||
const MIN_PREVIEW_WIDTH = 120
|
||||
const MAX_PREVIEW_WIDTH = 240
|
||||
|
||||
this.previewContainer.addEventListener('wheel', (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const delta = event.deltaY
|
||||
const oldWidth = this.previewWidth
|
||||
|
||||
if (delta > 0) {
|
||||
this.previewWidth = Math.max(MIN_PREVIEW_WIDTH, this.previewWidth - 10)
|
||||
} else {
|
||||
this.previewWidth = Math.min(MAX_PREVIEW_WIDTH, this.previewWidth + 10)
|
||||
}
|
||||
|
||||
if (
|
||||
oldWidth !== this.previewWidth &&
|
||||
(this.previewWidth === MIN_PREVIEW_WIDTH ||
|
||||
this.previewWidth === MAX_PREVIEW_WIDTH)
|
||||
) {
|
||||
this.flashPreviewBorder()
|
||||
}
|
||||
|
||||
this.updatePreviewSize()
|
||||
this.updatePreviewRender()
|
||||
})
|
||||
|
||||
this.previewContainer.style.display = this.showPreview ? 'block' : 'none'
|
||||
|
||||
container.appendChild(this.previewContainer)
|
||||
}
|
||||
|
||||
flashPreviewBorder() {
|
||||
const originalBorder = this.previewContainer.style.border
|
||||
const originalBoxShadow = this.previewContainer.style.boxShadow
|
||||
|
||||
this.previewContainer.style.border = '2px solid rgba(255, 255, 255, 0.8)'
|
||||
this.previewContainer.style.boxShadow = '0 0 8px rgba(255, 255, 255, 0.5)'
|
||||
|
||||
setTimeout(() => {
|
||||
this.previewContainer.style.border = originalBorder
|
||||
this.previewContainer.style.boxShadow = originalBoxShadow
|
||||
}, 100)
|
||||
}
|
||||
|
||||
updatePreviewRender() {
|
||||
if (!this.previewRenderer || !this.previewContainer || !this.showPreview)
|
||||
return
|
||||
@@ -254,19 +297,19 @@ class Load3d {
|
||||
|
||||
this.previewCamera.lookAt(this.controls.target)
|
||||
|
||||
const previewWidth = 120
|
||||
const previewHeight = (previewWidth * this.targetHeight) / this.targetWidth
|
||||
this.previewRenderer.setSize(previewWidth, previewHeight, false)
|
||||
const previewHeight =
|
||||
(this.previewWidth * this.targetHeight) / this.targetWidth
|
||||
this.previewRenderer.setSize(this.previewWidth, previewHeight, false)
|
||||
this.previewRenderer.render(this.scene, this.previewCamera)
|
||||
}
|
||||
|
||||
updatePreviewSize() {
|
||||
if (!this.previewContainer) return
|
||||
|
||||
const previewWidth = 120
|
||||
const previewHeight = (previewWidth * this.targetHeight) / this.targetWidth
|
||||
const previewHeight =
|
||||
(this.previewWidth * this.targetHeight) / this.targetWidth
|
||||
|
||||
this.previewRenderer?.setSize(previewWidth, previewHeight, false)
|
||||
this.previewRenderer?.setSize(this.previewWidth, previewHeight, false)
|
||||
}
|
||||
|
||||
setTargetSize(width: number, height: number) {
|
||||
|
||||
@@ -1,22 +1,41 @@
|
||||
import { ComfyNodeDef, InputSpec } from '@/types/apiTypes'
|
||||
import { ComfyNodeDef, InputSpec, isComboInputSpecV1 } from '@/types/apiTypes'
|
||||
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
// Adds an upload button to the nodes
|
||||
|
||||
const isImageComboInput = (inputSpec: InputSpec) => {
|
||||
const [inputName, inputOptions] = inputSpec
|
||||
if (!inputOptions || inputOptions['image_upload'] !== true) return false
|
||||
return isComboInputSpecV1(inputSpec) || inputName === 'COMBO'
|
||||
}
|
||||
|
||||
const createUploadInput = (
|
||||
imageInputName: string,
|
||||
imageInputOptions: InputSpec
|
||||
): InputSpec => [
|
||||
'IMAGEUPLOAD',
|
||||
{
|
||||
...imageInputOptions[1],
|
||||
imageInputName
|
||||
}
|
||||
]
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Comfy.UploadImage',
|
||||
beforeRegisterNodeDef(nodeType, nodeData: ComfyNodeDef) {
|
||||
// Check if there is a required input named 'image' in the nodeData
|
||||
const imageInputSpec: InputSpec | undefined =
|
||||
nodeData?.input?.required?.image
|
||||
const { input } = nodeData ?? {}
|
||||
const { required } = input ?? {}
|
||||
if (!required) return
|
||||
|
||||
// Get the config from the image input spec if it exists
|
||||
const config = imageInputSpec?.[1] ?? {}
|
||||
const { image_upload = false, image_folder = 'input' } = config
|
||||
const found = Object.entries(required).find(([_, input]) =>
|
||||
isImageComboInput(input)
|
||||
)
|
||||
|
||||
if (image_upload && nodeData?.input?.required) {
|
||||
nodeData.input.required.upload = ['IMAGEUPLOAD', { image_folder }]
|
||||
// If image combo input found, attach upload input
|
||||
if (found) {
|
||||
const [inputName, inputSpec] = found
|
||||
required.upload = createUploadInput(inputName, inputSpec)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "Browse Templates"
|
||||
},
|
||||
"Comfy_Canvas_DeleteSelectedItems": {
|
||||
"label": "Delete Selected Items"
|
||||
},
|
||||
"Comfy_Canvas_FitView": {
|
||||
"label": "Fit view to selected nodes"
|
||||
},
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
"updateConsent": "You previously opted in to reporting crashes. We are now tracking event-based metrics to help identify bugs and improve the app. No personal identifiable information is collected."
|
||||
},
|
||||
"serverStart": {
|
||||
"reinstall": "Reinstall",
|
||||
"troubleshoot": "Troubleshoot",
|
||||
"reportIssue": "Report Issue",
|
||||
"openLogs": "Open Logs",
|
||||
"showTerminal": "Show Terminal",
|
||||
@@ -473,6 +473,7 @@
|
||||
"Reinstall": "Reinstall",
|
||||
"Restart": "Restart",
|
||||
"Browse Templates": "Browse Templates",
|
||||
"Delete Selected Items": "Delete Selected Items",
|
||||
"Fit view to selected nodes": "Fit view to selected nodes",
|
||||
"Reset View": "Reset View",
|
||||
"Canvas Toggle Link Visibility": "Canvas Toggle Link Visibility",
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
"custom": "custom"
|
||||
}
|
||||
},
|
||||
"Comfy_Canvas_SelectionToolbox": {
|
||||
"name": "Show selection toolbox"
|
||||
},
|
||||
"Comfy_ConfirmClear": {
|
||||
"name": "Require confirmation when clearing workflow"
|
||||
},
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "Parcourir les modèles"
|
||||
},
|
||||
"Comfy_Canvas_DeleteSelectedItems": {
|
||||
"label": "Supprimer les éléments sélectionnés"
|
||||
},
|
||||
"Comfy_Canvas_FitView": {
|
||||
"label": "Ajuster la vue aux nœuds sélectionnés"
|
||||
},
|
||||
|
||||
@@ -378,6 +378,7 @@
|
||||
"ComfyUI Forum": "Forum ComfyUI",
|
||||
"ComfyUI Issues": "Problèmes de ComfyUI",
|
||||
"Convert selected nodes to group node": "Convertir les nœuds sélectionnés en nœud de groupe",
|
||||
"Delete Selected Items": "Supprimer les éléments sélectionnés",
|
||||
"Desktop User Guide": "Guide de l'utilisateur de bureau",
|
||||
"Duplicate Current Workflow": "Dupliquer le flux de travail actuel",
|
||||
"Edit": "Éditer",
|
||||
@@ -653,9 +654,9 @@
|
||||
"ready": "Finalisation...",
|
||||
"starting-server": "Démarrage du serveur ComfyUI..."
|
||||
},
|
||||
"reinstall": "Réinstaller",
|
||||
"reportIssue": "Signaler un problème",
|
||||
"showTerminal": "Afficher le terminal"
|
||||
"showTerminal": "Afficher le terminal",
|
||||
"troubleshoot": "Dépannage"
|
||||
},
|
||||
"settingsCategories": {
|
||||
"About": "À Propos",
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
},
|
||||
"tooltip": "Choisissez l'option personnalisée pour masquer la barre de titre du système"
|
||||
},
|
||||
"Comfy_Canvas_SelectionToolbox": {
|
||||
"name": "Afficher la boîte à outils de sélection"
|
||||
},
|
||||
"Comfy_ConfirmClear": {
|
||||
"name": "Demander une confirmation lors de l'effacement du flux de travail"
|
||||
},
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "テンプレートを参照"
|
||||
},
|
||||
"Comfy_Canvas_DeleteSelectedItems": {
|
||||
"label": "選択したアイテムを削除"
|
||||
},
|
||||
"Comfy_Canvas_FitView": {
|
||||
"label": "選択したノードにビューを合わせる"
|
||||
},
|
||||
|
||||
@@ -378,6 +378,7 @@
|
||||
"ComfyUI Forum": "ComfyUI フォーラム",
|
||||
"ComfyUI Issues": "ComfyUIの問題",
|
||||
"Convert selected nodes to group node": "選択したノードをグループノードに変換",
|
||||
"Delete Selected Items": "選択したアイテムを削除",
|
||||
"Desktop User Guide": "デスクトップユーザーガイド",
|
||||
"Duplicate Current Workflow": "現在のワークフローを複製",
|
||||
"Edit": "編集",
|
||||
@@ -653,9 +654,9 @@
|
||||
"ready": "完了中...",
|
||||
"starting-server": "ComfyUIサーバーを起動中..."
|
||||
},
|
||||
"reinstall": "再インストール",
|
||||
"reportIssue": "問題を報告",
|
||||
"showTerminal": "ターミナルを表示"
|
||||
"showTerminal": "ターミナルを表示",
|
||||
"troubleshoot": "トラブルシューティング"
|
||||
},
|
||||
"settingsCategories": {
|
||||
"About": "情報",
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
},
|
||||
"tooltip": "システムタイトルバーを非表示にするにはカスタムオプションを選択してください"
|
||||
},
|
||||
"Comfy_Canvas_SelectionToolbox": {
|
||||
"name": "選択ツールボックスを表示"
|
||||
},
|
||||
"Comfy_ConfirmClear": {
|
||||
"name": "ワークフローをクリアする際に確認を要求する"
|
||||
},
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "템플릿 탐색"
|
||||
},
|
||||
"Comfy_Canvas_DeleteSelectedItems": {
|
||||
"label": "선택한 항목 삭제"
|
||||
},
|
||||
"Comfy_Canvas_FitView": {
|
||||
"label": "선택한 노드에 뷰 맞추기"
|
||||
},
|
||||
|
||||
@@ -378,6 +378,7 @@
|
||||
"ComfyUI Forum": "ComfyUI 포럼",
|
||||
"ComfyUI Issues": "ComfyUI 이슈 페이지",
|
||||
"Convert selected nodes to group node": "선택한 노드를 그룹 노드로 변환",
|
||||
"Delete Selected Items": "선택한 항목 삭제",
|
||||
"Desktop User Guide": "데스크톱 사용자 가이드",
|
||||
"Duplicate Current Workflow": "현재 워크플로 복제",
|
||||
"Edit": "편집",
|
||||
@@ -653,9 +654,9 @@
|
||||
"ready": "마무리 중...",
|
||||
"starting-server": "ComfyUI 서버 시작 중..."
|
||||
},
|
||||
"reinstall": "재설치",
|
||||
"reportIssue": "문제 보고",
|
||||
"showTerminal": "터미널 보기"
|
||||
"showTerminal": "터미널 보기",
|
||||
"troubleshoot": "문제 해결"
|
||||
},
|
||||
"settingsCategories": {
|
||||
"About": "정보",
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
},
|
||||
"tooltip": "시스템 제목 표시 줄을 숨기려면 사용자 정의 옵션을 선택하세요"
|
||||
},
|
||||
"Comfy_Canvas_SelectionToolbox": {
|
||||
"name": "선택 도구 상자 표시"
|
||||
},
|
||||
"Comfy_ConfirmClear": {
|
||||
"name": "워크플로 비우기 시 확인 요구"
|
||||
},
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "Просмотр шаблонов"
|
||||
},
|
||||
"Comfy_Canvas_DeleteSelectedItems": {
|
||||
"label": "Удалить выбранные элементы"
|
||||
},
|
||||
"Comfy_Canvas_FitView": {
|
||||
"label": "Подогнать вид к выбранным нодам"
|
||||
},
|
||||
|
||||
@@ -378,6 +378,7 @@
|
||||
"ComfyUI Forum": "Форум ComfyUI",
|
||||
"ComfyUI Issues": "Проблемы ComfyUI",
|
||||
"Convert selected nodes to group node": "Преобразовать выбранные ноды в групповую ноду",
|
||||
"Delete Selected Items": "Удалить выбранные элементы",
|
||||
"Desktop User Guide": "Руководство пользователя для настольных ПК",
|
||||
"Duplicate Current Workflow": "Дублировать текущий рабочий процесс",
|
||||
"Edit": "Редактировать",
|
||||
@@ -653,9 +654,9 @@
|
||||
"ready": "Завершение…",
|
||||
"starting-server": "Запуск сервера ComfyUI…"
|
||||
},
|
||||
"reinstall": "Переустановить",
|
||||
"reportIssue": "Сообщить о проблеме",
|
||||
"showTerminal": "Показать терминал"
|
||||
"showTerminal": "Показать терминал",
|
||||
"troubleshoot": "Устранение неполадок"
|
||||
},
|
||||
"settingsCategories": {
|
||||
"About": "О программе",
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
},
|
||||
"tooltip": "Выберите пользовательский вариант, чтобы скрыть системную строку заголовка"
|
||||
},
|
||||
"Comfy_Canvas_SelectionToolbox": {
|
||||
"name": "Показать панель инструментов выбора"
|
||||
},
|
||||
"Comfy_ConfirmClear": {
|
||||
"name": "Требовать подтверждение при очистке рабочего процесса"
|
||||
},
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "浏览模板"
|
||||
},
|
||||
"Comfy_Canvas_DeleteSelectedItems": {
|
||||
"label": "删除选定的项目"
|
||||
},
|
||||
"Comfy_Canvas_FitView": {
|
||||
"label": "适应视图到选中节点"
|
||||
},
|
||||
|
||||
@@ -378,6 +378,7 @@
|
||||
"ComfyUI Forum": "ComfyUI 论坛",
|
||||
"ComfyUI Issues": "ComfyUI 问题",
|
||||
"Convert selected nodes to group node": "将选中节点转换为组节点",
|
||||
"Delete Selected Items": "删除选定的项目",
|
||||
"Desktop User Guide": "桌面端用户指南",
|
||||
"Duplicate Current Workflow": "复制当前工作流",
|
||||
"Edit": "编辑",
|
||||
@@ -653,9 +654,9 @@
|
||||
"ready": "完成中...",
|
||||
"starting-server": "正在启动 ComfyUI 服务器..."
|
||||
},
|
||||
"reinstall": "重新安装",
|
||||
"reportIssue": "报告问题",
|
||||
"showTerminal": "显示终端"
|
||||
"showTerminal": "显示终端",
|
||||
"troubleshoot": "故障排除"
|
||||
},
|
||||
"settingsCategories": {
|
||||
"About": "关于",
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
},
|
||||
"tooltip": "选择自定义选项以隐藏系统标题栏"
|
||||
},
|
||||
"Comfy_Canvas_SelectionToolbox": {
|
||||
"name": "显示选择工具箱"
|
||||
},
|
||||
"Comfy_ConfirmClear": {
|
||||
"name": "清除工作流时需要确认"
|
||||
},
|
||||
|
||||
@@ -27,6 +27,7 @@ import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { useWidgetStore } from '@/stores/widgetStore'
|
||||
import { ComfyWorkflow } from '@/stores/workflowStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import type { ComfyNodeDef } from '@/types/apiTypes'
|
||||
import type { ComfyExtension, MissingNodeType } from '@/types/comfy'
|
||||
@@ -650,7 +651,7 @@ export class ComfyApp {
|
||||
const opacity = useSettingStore().get('Comfy.Node.Opacity')
|
||||
if (opacity) adjustments.opacity = opacity
|
||||
|
||||
if (useSettingStore().get('Comfy.ColorPalette') === 'light') {
|
||||
if (useColorPaletteStore().completedActivePalette.light_theme) {
|
||||
adjustments.lightness = 0.5
|
||||
|
||||
// Lighten title bar of colored nodes on light theme
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { LGraphCanvas, LGraphGroup, LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { LGraphCanvas, LGraphGroup, LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { Positionable } from '@comfyorg/litegraph/dist/interfaces'
|
||||
import { defineStore } from 'pinia'
|
||||
import { shallowRef } from 'vue'
|
||||
import { markRaw, ref, shallowRef } from 'vue'
|
||||
|
||||
export const useTitleEditorStore = defineStore('titleEditor', () => {
|
||||
const titleEditorTarget = shallowRef<LGraphNode | LGraphGroup | null>(null)
|
||||
@@ -17,8 +18,18 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||
* The root LGraphCanvas object is shallow reactive.
|
||||
*/
|
||||
const canvas = shallowRef<LGraphCanvas | null>(null)
|
||||
/**
|
||||
* The selected items on the canvas. All stored items are raw.
|
||||
*/
|
||||
const selectedItems = ref<Positionable[]>([])
|
||||
const updateSelectedItems = () => {
|
||||
const items = Array.from(canvas.value?.selectedItems ?? [])
|
||||
selectedItems.value = items.map((item) => markRaw(item))
|
||||
}
|
||||
|
||||
return {
|
||||
canvas
|
||||
canvas,
|
||||
selectedItems,
|
||||
updateSelectedItems
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3,15 +3,14 @@ import { defineStore } from 'pinia'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
import { ExecutedWsMessage, ResultItem } from '@/types/apiTypes'
|
||||
import { parseFilePath } from '@/utils/formatUtil'
|
||||
|
||||
const toOutputs = (
|
||||
filenames: string[],
|
||||
type: string
|
||||
): ExecutedWsMessage['output'] => {
|
||||
return {
|
||||
images: filenames.map((image) => {
|
||||
return { filename: image, subfolder: '', type }
|
||||
})
|
||||
images: filenames.map((image) => ({ type, ...parseFilePath(image) }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -349,6 +349,7 @@ const zComboInputProps = zBaseInputSpecValue.extend({
|
||||
control_after_generate: z.boolean().optional(),
|
||||
image_upload: z.boolean().optional(),
|
||||
image_folder: z.enum(['input', 'output', 'temp']).optional(),
|
||||
allow_batch: z.boolean().optional(),
|
||||
remote: zRemoteWidgetConfig.optional()
|
||||
})
|
||||
|
||||
@@ -590,7 +591,8 @@ const zSettings = z.record(z.any()).and(
|
||||
'LiteGraph.Canvas.MaximumFps': z.number(),
|
||||
'Comfy.Workflow.ConfirmDelete': z.boolean(),
|
||||
'Comfy.RerouteBeta': z.boolean(),
|
||||
'LiteGraph.Canvas.LowQualityRenderingZoomThreshold': z.number()
|
||||
'LiteGraph.Canvas.LowQualityRenderingZoomThreshold': z.number(),
|
||||
'Comfy.Canvas.SelectionToolbox': z.boolean()
|
||||
})
|
||||
.optional()
|
||||
)
|
||||
|
||||
7
src/types/litegraph-augmentation.d.ts
vendored
@@ -22,6 +22,11 @@ declare module '@comfyorg/litegraph/dist/types/widgets' {
|
||||
index: number
|
||||
) => Promise<unknown> | unknown
|
||||
|
||||
/**
|
||||
* Refreshes the widget's value or options from its remote source.
|
||||
*/
|
||||
refresh?: () => unknown
|
||||
|
||||
/**
|
||||
* If the widget supports dynamic prompts, this will be set to true.
|
||||
* See extensions/core/dynamicPrompts.ts
|
||||
@@ -122,6 +127,8 @@ declare module '@comfyorg/litegraph' {
|
||||
imageOffset?: number
|
||||
/** Callback for pasting an image file into the node */
|
||||
pasteFile?(file: File): void
|
||||
/** Callback for pasting multiple files into the node */
|
||||
pasteFiles?(files: File[]): void
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
ElectronAPI,
|
||||
ElectronContextMenuOptions
|
||||
} from '@comfyorg/comfyui-electron-types'
|
||||
import { ElectronAPI } from '@comfyorg/comfyui-electron-types'
|
||||
|
||||
export function isElectron() {
|
||||
return 'electronAPI' in window && window.electronAPI !== undefined
|
||||
@@ -11,8 +8,8 @@ export function electronAPI() {
|
||||
return (window as any).electronAPI as ElectronAPI
|
||||
}
|
||||
|
||||
export function showNativeMenu(event: MouseEvent) {
|
||||
electronAPI()?.showContextMenu(event as ElectronContextMenuOptions)
|
||||
export function showNativeSystemMenu() {
|
||||
electronAPI()?.showContextMenu()
|
||||
}
|
||||
|
||||
export function isNativeWindow() {
|
||||
|
||||
@@ -232,3 +232,41 @@ export function createAnnotatedPath(
|
||||
return `${createPath(item, subfolder)}${createAnnotation(rootFolder)}`
|
||||
return `${createPath(item.filename ?? '', item.subfolder)}${createAnnotation(item.type)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a filepath into its filename and subfolder components.
|
||||
*
|
||||
* @example
|
||||
* parseFilePath('folder/file.txt') // → { filename: 'file.txt', subfolder: 'folder' }
|
||||
* parseFilePath('/folder/file.txt') // → { filename: 'file.txt', subfolder: 'folder' }
|
||||
* parseFilePath('file.txt') // → { filename: 'file.txt', subfolder: '' }
|
||||
* parseFilePath('folder//file.txt') // → { filename: 'file.txt', subfolder: 'folder' }
|
||||
*
|
||||
* @param filepath The filepath to parse
|
||||
* @returns Object containing filename and subfolder
|
||||
*/
|
||||
export function parseFilePath(filepath: string): {
|
||||
filename: string
|
||||
subfolder: string
|
||||
} {
|
||||
if (!filepath?.trim()) return { filename: '', subfolder: '' }
|
||||
|
||||
const normalizedPath = filepath
|
||||
.replace(/[\\/]+/g, '/') // Normalize path separators
|
||||
.replace(/^\//, '') // Remove leading slash
|
||||
.replace(/\/$/, '') // Remove trailing slash
|
||||
|
||||
const lastSlashIndex = normalizedPath.lastIndexOf('/')
|
||||
|
||||
if (lastSlashIndex === -1) {
|
||||
return {
|
||||
filename: normalizedPath,
|
||||
subfolder: ''
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
filename: normalizedPath.slice(lastSlashIndex + 1),
|
||||
subfolder: normalizedPath.slice(0, lastSlashIndex)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { IWidget, LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
export function isImageNode(node: LGraphNode) {
|
||||
return (
|
||||
@@ -8,3 +9,16 @@ export function isImageNode(node: LGraphNode) {
|
||||
node.widgets.findIndex((obj: IWidget) => obj.name === 'image') >= 0)
|
||||
)
|
||||
}
|
||||
|
||||
export function addToComboValues(widget: IComboWidget, value: string) {
|
||||
if (!widget.options) widget.options = { values: [] }
|
||||
if (!widget.options.values) widget.options.values = []
|
||||
if (!widget.options.values.includes(value)) {
|
||||
widget.options.values.push(value)
|
||||
}
|
||||
}
|
||||
|
||||
export const isLGraphNode = (item: unknown): item is LGraphNode => {
|
||||
const name = item?.constructor?.name
|
||||
return name === 'ComfyNode' || name === 'LGraphNode'
|
||||
}
|
||||
|
||||
@@ -135,12 +135,12 @@ const filterOptions = ref([
|
||||
])
|
||||
|
||||
/** Filter binding; can be set to show all tasks, or only errors. */
|
||||
const filter = ref<MaintenanceFilter>(filterOptions.value[1])
|
||||
const filter = ref<MaintenanceFilter>(filterOptions.value[0])
|
||||
|
||||
/** If valid, leave the validation window. */
|
||||
const completeValidation = async (alertOnFail = true) => {
|
||||
const completeValidation = async () => {
|
||||
const isValid = await electron.Validation.complete()
|
||||
if (alertOnFail && !isValid) {
|
||||
if (!isValid) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
@@ -162,18 +162,13 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// If we're running a fix that may resolve all issues, auto-recheck and continue if everything is OK
|
||||
watch(
|
||||
() => taskStore.isRunningInstallationFix,
|
||||
(value, oldValue) => {
|
||||
if (!value && oldValue) completeValidation(false)
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
electron.Validation.onUpdate(processUpdate)
|
||||
|
||||
const update = await electron.Validation.getStatus()
|
||||
if (Object.values(update).some((x) => x === 'error')) {
|
||||
filter.value = filterOptions.value[1]
|
||||
}
|
||||
processUpdate(update)
|
||||
})
|
||||
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
@click="openLogs"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
:label="t('serverStart.reinstall')"
|
||||
@click="reinstall"
|
||||
icon="pi pi-wrench"
|
||||
:label="t('serverStart.troubleshoot')"
|
||||
@click="troubleshoot"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
@@ -88,7 +88,7 @@ const terminalCreated = (
|
||||
terminal.options.cursorInactiveStyle = 'block'
|
||||
}
|
||||
|
||||
const reinstall = () => electron.reinstall()
|
||||
const troubleshoot = () => electron.startTroubleshooting()
|
||||
const reportIssue = () => {
|
||||
window.open('https://forum.comfy.org/c/v1-feedback/', '_blank')
|
||||
}
|
||||
|
||||