mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-03 11:09:10 +00:00
Compare commits
21 Commits
v1.3.41
...
node-templ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
782d93a7a0 | ||
|
|
7be14c5189 | ||
|
|
ee5c127146 | ||
|
|
acba6097e0 | ||
|
|
82d00a1bcf | ||
|
|
b9224464c0 | ||
|
|
fba9a03df3 | ||
|
|
2fd624cd3d | ||
|
|
095fe2a175 | ||
|
|
d838777e04 | ||
|
|
7e0d1d441d | ||
|
|
ddab149f16 | ||
|
|
a73fdcd3bd | ||
|
|
d6e0c197bd | ||
|
|
3117d0fdc1 | ||
|
|
96fda64b70 | ||
|
|
e3d2c3a814 | ||
|
|
1a8900de1f | ||
|
|
05ba526388 | ||
|
|
4bc79181ae | ||
|
|
feafbf9cbf |
22
README.md
22
README.md
@@ -425,9 +425,29 @@ hook is used to auto-format code on commit.
|
||||
Note: The dev server will NOT load any extension from the ComfyUI server. Only
|
||||
core extensions will be loaded.
|
||||
|
||||
- Run `npm install` to install the necessary packages
|
||||
- Start local ComfyUI backend at `localhost:8188`
|
||||
- Run `npm run dev` to start the dev server
|
||||
- Run `npm run dev:electron` to start the dev server with electron API mocked
|
||||
|
||||
#### Access dev server on touch devices
|
||||
|
||||
After you start the dev server, you should see following logs:
|
||||
|
||||
```
|
||||
> comfyui-frontend@1.3.42 dev
|
||||
> vite
|
||||
|
||||
|
||||
VITE v5.4.6 ready in 488 ms
|
||||
|
||||
➜ Local: http://localhost:5173/
|
||||
➜ Network: http://172.21.80.1:5173/
|
||||
➜ Network: http://192.168.2.20:5173/
|
||||
➜ press h + enter to show help
|
||||
```
|
||||
|
||||
Make sure your desktop machine and touch device are on the same network. On your touch device,
|
||||
navigate to `http://<server_ip>:5173` (e.g. `http://192.168.2.20:5173` here), to access the ComfyUI frontend.
|
||||
|
||||
### Unit Test
|
||||
|
||||
|
||||
10
browser_tests/assets/node_template_templates.json
Normal file
10
browser_tests/assets/node_template_templates.json
Normal file
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"name": "Three Nodes Template",
|
||||
"data": "{\"nodes\":[{\"id\":7,\"type\":\"CLIPTextEncode\",\"pos\":[413,389],\"size\":[425.27801513671875,180.6060791015625],\"flags\":{},\"order\":3,\"mode\":0,\"inputs\":[{\"name\":\"clip\",\"type\":\"CLIP\",\"link\":null}],\"outputs\":[{\"name\":\"CONDITIONING\",\"type\":\"CONDITIONING\",\"links\":[],\"slot_index\":0}],\"properties\":{\"Node name for S&R\":\"CLIPTextEncode\"},\"widgets_values\":[\"text, watermark\"]},{\"id\":6,\"type\":\"CLIPTextEncode\",\"pos\":[415,186],\"size\":[422.84503173828125,164.31304931640625],\"flags\":{},\"order\":2,\"mode\":0,\"inputs\":[{\"name\":\"clip\",\"type\":\"CLIP\",\"link\":null}],\"outputs\":[{\"name\":\"CONDITIONING\",\"type\":\"CONDITIONING\",\"links\":[],\"slot_index\":0}],\"properties\":{\"Node name for S&R\":\"CLIPTextEncode\"},\"widgets_values\":[\"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,\"]},{\"id\":4,\"type\":\"CheckpointLoaderSimple\",\"pos\":[26,474],\"size\":[315,98],\"flags\":{},\"order\":1,\"mode\":0,\"inputs\":[],\"outputs\":[{\"name\":\"MODEL\",\"type\":\"MODEL\",\"links\":[],\"slot_index\":0},{\"name\":\"CLIP\",\"type\":\"CLIP\",\"links\":[],\"slot_index\":1},{\"name\":\"VAE\",\"type\":\"VAE\",\"links\":[],\"slot_index\":2}],\"properties\":{\"Node name for S&R\":\"CheckpointLoaderSimple\"},\"widgets_values\":[\"v1-5-pruned-emaonly.ckpt\"]}],\"groups\":[],\"reroutes\":[],\"links\":[{\"id\":5,\"origin_id\":4,\"origin_slot\":1,\"target_id\":7,\"target_slot\":0,\"type\":\"CLIP\"},{\"id\":3,\"origin_id\":4,\"origin_slot\":1,\"target_id\":6,\"target_slot\":0,\"type\":\"CLIP\"}]}"
|
||||
},
|
||||
{
|
||||
"name": "Completely empty template",
|
||||
"data": "{\"nodes\":[],\"groups\":[],\"reroutes\":[],\"links\":[]}"
|
||||
}
|
||||
]
|
||||
6
browser_tests/assets/vintage_clipboard_template.json
Normal file
6
browser_tests/assets/vintage_clipboard_template.json
Normal file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"name": "vintageClipboard Template",
|
||||
"data": "{\"nodes\":[{\"id\":-1,\"type\":\"CheckpointLoaderSimple\",\"pos\":[26,474],\"size\":[315,98],\"flags\":{},\"order\":1,\"mode\":0,\"inputs\":[],\"outputs\":[{\"name\":\"MODEL\",\"type\":\"MODEL\",\"links\":[],\"slot_index\":0},{\"name\":\"CLIP\",\"type\":\"CLIP\",\"links\":[],\"slot_index\":1},{\"name\":\"VAE\",\"type\":\"VAE\",\"links\":[],\"slot_index\":2}],\"properties\":{\"Node name for S&R\":\"CheckpointLoaderSimple\"},\"widgets_values\":[\"v1-5-pruned-emaonly.ckpt\"]},{\"id\":-1,\"type\":\"CLIPTextEncode\",\"pos\":[415,186],\"size\":[422.84503173828125,164.31304931640625],\"flags\":{},\"order\":2,\"mode\":0,\"inputs\":[{\"name\":\"clip\",\"type\":\"CLIP\",\"link\":null}],\"outputs\":[{\"name\":\"CONDITIONING\",\"type\":\"CONDITIONING\",\"links\":[],\"slot_index\":0}],\"properties\":{\"Node name for S&R\":\"CLIPTextEncode\"},\"widgets_values\":[\"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,\"]},{\"id\":-1,\"type\":\"CLIPTextEncode\",\"pos\":[413,389],\"size\":[425.27801513671875,180.6060791015625],\"flags\":{},\"order\":3,\"mode\":0,\"inputs\":[{\"name\":\"clip\",\"type\":\"CLIP\",\"link\":null}],\"outputs\":[{\"name\":\"CONDITIONING\",\"type\":\"CONDITIONING\",\"links\":[],\"slot_index\":0}],\"properties\":{\"Node name for S&R\":\"CLIPTextEncode\"},\"widgets_values\":[\"text, watermark\"]}],\"links\":[[0,1,1,0,4],[0,1,2,0,4]]}"
|
||||
}
|
||||
]
|
||||
@@ -3,6 +3,9 @@ import {
|
||||
comfyPageFixture as test,
|
||||
comfyExpect as expect
|
||||
} from './fixtures/ComfyPage'
|
||||
import type { useWorkspaceStore } from '../src/stores/workspaceStore'
|
||||
|
||||
type WorkspaceStore = ReturnType<typeof useWorkspaceStore>
|
||||
|
||||
async function beforeChange(comfyPage: ComfyPage) {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
@@ -19,38 +22,40 @@ test.describe('Change Tracker', () => {
|
||||
test.describe('Undo/Redo', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setupWorkflowsDirectory({})
|
||||
})
|
||||
|
||||
test('Can undo multiple operations', async ({ comfyPage }) => {
|
||||
function isModified() {
|
||||
return comfyPage.page.evaluate(async () => {
|
||||
return window['app'].extensionManager.workflow.activeWorkflow
|
||||
.isModified
|
||||
return !!(window['app'].extensionManager as WorkspaceStore).workflow
|
||||
.activeWorkflow?.isModified
|
||||
})
|
||||
}
|
||||
|
||||
function getUndoQueueSize() {
|
||||
return comfyPage.page.evaluate(() => {
|
||||
const workflow =
|
||||
window['app'].extensionManager.workflow.activeWorkflow
|
||||
return workflow.changeTracker.undoQueue.length
|
||||
const workflow = (window['app'].extensionManager as WorkspaceStore)
|
||||
.workflow.activeWorkflow
|
||||
return workflow?.changeTracker.undoQueue.length
|
||||
})
|
||||
}
|
||||
|
||||
function getRedoQueueSize() {
|
||||
return comfyPage.page.evaluate(() => {
|
||||
const workflow =
|
||||
window['app'].extensionManager.workflow.activeWorkflow
|
||||
return workflow.changeTracker.redoQueue.length
|
||||
const workflow = (window['app'].extensionManager as WorkspaceStore)
|
||||
.workflow.activeWorkflow
|
||||
return workflow?.changeTracker.redoQueue.length
|
||||
})
|
||||
}
|
||||
expect(await getUndoQueueSize()).toBe(0)
|
||||
expect(await getRedoQueueSize()).toBe(0)
|
||||
|
||||
// Save, confirm no errors & workflow modified flag removed
|
||||
await comfyPage.menu.topbar.saveWorkflow('undo-redo-test')
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => !window['app'].extensionManager.workflow.activeWorkflow.isModified
|
||||
)
|
||||
expect(await comfyPage.getToastErrorCount()).toBe(0)
|
||||
expect(await isModified()).toBe(false)
|
||||
|
||||
// TODO(huchenlei): Investigate why saving the workflow is causing the
|
||||
// undo queue to be triggered.
|
||||
expect(await getUndoQueueSize()).toBe(1)
|
||||
@@ -75,13 +80,11 @@ test.describe('Change Tracker', () => {
|
||||
expect(await getUndoQueueSize()).toBe(2)
|
||||
expect(await getRedoQueueSize()).toBe(1)
|
||||
|
||||
// TODO(huchenlei): Following assertion is flaky.
|
||||
// Seems like ctrlZ() is not triggered correctly.
|
||||
// await comfyPage.ctrlZ()
|
||||
// await expect(node).not.toBeCollapsed()
|
||||
// expect(await isModified()).toBe(false)
|
||||
// expect(await getUndoQueueSize()).toBe(1)
|
||||
// expect(await getRedoQueueSize()).toBe(2)
|
||||
await comfyPage.ctrlZ()
|
||||
await expect(node).not.toBeCollapsed()
|
||||
expect(await isModified()).toBe(false)
|
||||
expect(await getUndoQueueSize()).toBe(1)
|
||||
expect(await getRedoQueueSize()).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -109,7 +112,7 @@ test.describe('Change Tracker', () => {
|
||||
await expect(node).not.toBeCollapsed()
|
||||
|
||||
// Run again, but within a change transaction
|
||||
beforeChange(comfyPage)
|
||||
await beforeChange(comfyPage)
|
||||
|
||||
await node.click('collapse')
|
||||
await comfyPage.ctrlB()
|
||||
@@ -117,7 +120,7 @@ test.describe('Change Tracker', () => {
|
||||
await expect(node).toBeBypassed()
|
||||
|
||||
// End transaction
|
||||
afterChange(comfyPage)
|
||||
await afterChange(comfyPage)
|
||||
|
||||
// Ensure undo reverts both changes
|
||||
await comfyPage.ctrlZ()
|
||||
@@ -125,7 +128,7 @@ test.describe('Change Tracker', () => {
|
||||
await expect(node).not.toBeCollapsed()
|
||||
})
|
||||
|
||||
test('Can group multiple transaction calls into a single one', async ({
|
||||
test('Can nest multiple change transactions without adding undo steps', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const node = (await comfyPage.getFirstNodeRef())!
|
||||
|
||||
@@ -15,9 +15,9 @@ test.describe('Copy Paste', () => {
|
||||
await textBox.click()
|
||||
const originalString = await textBox.inputValue()
|
||||
await textBox.selectText()
|
||||
await comfyPage.ctrlC()
|
||||
await comfyPage.ctrlV()
|
||||
await comfyPage.ctrlV()
|
||||
await comfyPage.ctrlC(null)
|
||||
await comfyPage.ctrlV(null)
|
||||
await comfyPage.ctrlV(null)
|
||||
const resultString = await textBox.inputValue()
|
||||
expect(resultString).toBe(originalString + originalString)
|
||||
})
|
||||
@@ -31,7 +31,7 @@ test.describe('Copy Paste', () => {
|
||||
y: 643
|
||||
}
|
||||
})
|
||||
await comfyPage.ctrlC()
|
||||
await comfyPage.ctrlC(null)
|
||||
// KSampler's seed
|
||||
await comfyPage.canvas.click({
|
||||
position: {
|
||||
@@ -39,7 +39,7 @@ test.describe('Copy Paste', () => {
|
||||
y: 281
|
||||
}
|
||||
})
|
||||
await comfyPage.ctrlV()
|
||||
await comfyPage.ctrlV(null)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('copied-widget-value.png')
|
||||
})
|
||||
@@ -51,14 +51,14 @@ test.describe('Copy Paste', () => {
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.clickEmptyLatentNode()
|
||||
await comfyPage.ctrlC()
|
||||
await comfyPage.ctrlC(null)
|
||||
const textBox = comfyPage.widgetTextBox
|
||||
await textBox.click()
|
||||
await textBox.inputValue()
|
||||
await textBox.selectText()
|
||||
await comfyPage.ctrlC()
|
||||
await comfyPage.ctrlV()
|
||||
await comfyPage.ctrlV()
|
||||
await comfyPage.ctrlC(null)
|
||||
await comfyPage.ctrlV(null)
|
||||
await comfyPage.ctrlV(null)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'paste-in-text-area-with-node-previously-copied.png'
|
||||
)
|
||||
@@ -69,10 +69,10 @@ test.describe('Copy Paste', () => {
|
||||
await textBox.click()
|
||||
await textBox.inputValue()
|
||||
await textBox.selectText()
|
||||
await comfyPage.ctrlC()
|
||||
await comfyPage.ctrlC(null)
|
||||
// Unfocus textbox.
|
||||
await comfyPage.page.mouse.click(10, 10)
|
||||
await comfyPage.ctrlV()
|
||||
await comfyPage.ctrlV(null)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('no-node-copied.png')
|
||||
})
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ test.describe('Execution error', () => {
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution_error')
|
||||
await comfyPage.queueButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Wait for the element with the .comfy-execution-error selector to be visible
|
||||
const executionError = comfyPage.page.locator('.comfy-error-report')
|
||||
@@ -93,7 +94,7 @@ test.describe('Settings', () => {
|
||||
test('Can change canvas zoom speed setting', async ({ comfyPage }) => {
|
||||
const maxSpeed = 2.5
|
||||
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed)
|
||||
test.step('Setting should persist', async () => {
|
||||
await test.step('Setting should persist', async () => {
|
||||
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -48,7 +48,7 @@ test.describe('Topbar commands', () => {
|
||||
})
|
||||
})
|
||||
|
||||
const menuItem: Locator = await comfyPage.menu.topbar.getMenuItem('ext')
|
||||
const menuItem = comfyPage.menu.topbar.getMenuItem('ext')
|
||||
expect(await menuItem.count()).toBe(0)
|
||||
})
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ export class ComfyPage {
|
||||
// All canvas position operations are based on default view of canvas.
|
||||
public readonly canvas: Locator
|
||||
public readonly widgetTextBox: Locator
|
||||
public readonly contextMenu: Locator
|
||||
|
||||
// Buttons
|
||||
public readonly resetViewButton: Locator
|
||||
@@ -107,6 +108,7 @@ export class ComfyPage {
|
||||
this.url = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'
|
||||
this.canvas = page.locator('#graph-canvas')
|
||||
this.widgetTextBox = page.getByPlaceholder('text').nth(1)
|
||||
this.contextMenu = page.locator('.litegraph.litecontextmenu')
|
||||
this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
|
||||
this.queueButton = page.getByRole('button', { name: 'Queue Prompt' })
|
||||
this.workflowUploadInput = page.locator('#comfy-file-input')
|
||||
@@ -148,6 +150,12 @@ export class ComfyPage {
|
||||
})
|
||||
}
|
||||
|
||||
async getGraphSelectedItemsCount(): Promise<number | undefined> {
|
||||
return await this.page.evaluate(() => {
|
||||
return window['app']?.canvas?.selectedItems?.size
|
||||
})
|
||||
}
|
||||
|
||||
async setupWorkflowsDirectory(structure: FolderStructure) {
|
||||
const resp = await this.request.post(
|
||||
`${this.url}/api/devtools/setup_folder_structure`,
|
||||
@@ -191,6 +199,39 @@ export class ComfyPage {
|
||||
return await resp.json()
|
||||
}
|
||||
|
||||
async clearNodeTemplates() {
|
||||
const resp = await this.request.delete(
|
||||
`${this.url}/api/userdata/comfy.templates.json`,
|
||||
{
|
||||
headers: { 'Comfy-User': this.id }
|
||||
}
|
||||
)
|
||||
|
||||
const status = resp.status()
|
||||
if (status !== 204 && status !== 404)
|
||||
throw new Error(`Failed to delete node templates: ${await resp.text()}`)
|
||||
}
|
||||
|
||||
async setNodeTemplates(fileName: string) {
|
||||
const path = this.assetPath(fileName)
|
||||
const data = fs.readFileSync(path, 'utf-8')
|
||||
|
||||
const resp = await this.request.post(
|
||||
`${this.url}/api/userdata/comfy.templates.json`,
|
||||
{
|
||||
headers: {
|
||||
'Comfy-User': this.id,
|
||||
overwrite: 'true',
|
||||
full_info: 'true'
|
||||
},
|
||||
data
|
||||
}
|
||||
)
|
||||
|
||||
if (resp.status() !== 200)
|
||||
throw new Error(`Failed to upload node templates: ${await resp.text()}`)
|
||||
}
|
||||
|
||||
async setupSettings(settings: Record<string, any>) {
|
||||
const resp = await this.request.post(
|
||||
`${this.url}/api/devtools/set_settings`,
|
||||
@@ -349,6 +390,12 @@ export class ComfyPage {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async getToastErrorCount() {
|
||||
return await this.page
|
||||
.locator('.p-toast-message.p-toast-message-error')
|
||||
.count()
|
||||
}
|
||||
|
||||
async getVisibleToastCount() {
|
||||
return await this.page.locator('.p-toast:visible').count()
|
||||
}
|
||||
@@ -393,11 +440,17 @@ export class ComfyPage {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async dragAndDrop(source: Position, target: Position) {
|
||||
async dragAndDrop(
|
||||
source: Position,
|
||||
target: Position,
|
||||
modifierKey?: 'ControlOrMeta' | 'Control' | 'Alt' | 'Shift'
|
||||
) {
|
||||
if (modifierKey) await this.page.keyboard.down(modifierKey)
|
||||
await this.page.mouse.move(source.x, source.y)
|
||||
await this.page.mouse.down()
|
||||
await this.page.mouse.move(target.x, target.y)
|
||||
await this.page.mouse.up()
|
||||
if (modifierKey) await this.page.keyboard.up(modifierKey)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
@@ -545,12 +598,15 @@ export class ComfyPage {
|
||||
}
|
||||
|
||||
async rightClickCanvas() {
|
||||
await this.page.mouse.click(10, 10, { button: 'right' })
|
||||
await this.nextFrame()
|
||||
await this.canvas.click({
|
||||
position: { x: 10, y: 10 },
|
||||
button: 'right'
|
||||
})
|
||||
await expect(this.contextMenu).toBeVisible()
|
||||
}
|
||||
|
||||
async doubleClickCanvas() {
|
||||
await this.page.mouse.dblclick(10, 10)
|
||||
await this.page.mouse.dblclick(10, 10, { delay: 5 })
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
@@ -561,7 +617,7 @@ export class ComfyPage {
|
||||
y: 625
|
||||
}
|
||||
})
|
||||
this.page.mouse.move(10, 10)
|
||||
await this.page.mouse.move(10, 10)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
@@ -573,10 +629,14 @@ export class ComfyPage {
|
||||
},
|
||||
button: 'right'
|
||||
})
|
||||
this.page.mouse.move(10, 10)
|
||||
await this.page.mouse.move(10, 10)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async clickContextMenuItem(name: string): Promise<void> {
|
||||
await this.page.getByRole('menuitem', { name }).click()
|
||||
}
|
||||
|
||||
async select2Nodes() {
|
||||
// Select 2 CLIP nodes.
|
||||
await this.page.keyboard.down('Control')
|
||||
@@ -586,43 +646,42 @@ export class ComfyPage {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async ctrlSend(keyToPress: string) {
|
||||
await this.page.keyboard.down('Control')
|
||||
await this.page.keyboard.press(keyToPress)
|
||||
await this.page.keyboard.up('Control')
|
||||
async ctrlSend(keyToPress: string, locator: Locator | null = this.canvas) {
|
||||
const target = locator ?? this.page.keyboard
|
||||
await target.press(`Control+${keyToPress}`)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async ctrlA() {
|
||||
await this.ctrlSend('KeyA')
|
||||
async ctrlA(locator?: Locator | null) {
|
||||
await this.ctrlSend('KeyA', locator)
|
||||
}
|
||||
|
||||
async ctrlB() {
|
||||
await this.ctrlSend('KeyB')
|
||||
async ctrlB(locator?: Locator | null) {
|
||||
await this.ctrlSend('KeyB', locator)
|
||||
}
|
||||
|
||||
async ctrlC() {
|
||||
await this.ctrlSend('KeyC')
|
||||
async ctrlC(locator?: Locator | null) {
|
||||
await this.ctrlSend('KeyC', locator)
|
||||
}
|
||||
|
||||
async ctrlV() {
|
||||
await this.ctrlSend('KeyV')
|
||||
async ctrlV(locator?: Locator | null) {
|
||||
await this.ctrlSend('KeyV', locator)
|
||||
}
|
||||
|
||||
async ctrlZ() {
|
||||
await this.ctrlSend('KeyZ')
|
||||
async ctrlZ(locator?: Locator | null) {
|
||||
await this.ctrlSend('KeyZ', locator)
|
||||
}
|
||||
|
||||
async ctrlY() {
|
||||
await this.ctrlSend('KeyY')
|
||||
async ctrlY(locator?: Locator | null) {
|
||||
await this.ctrlSend('KeyY', locator)
|
||||
}
|
||||
|
||||
async ctrlArrowUp() {
|
||||
await this.ctrlSend('ArrowUp')
|
||||
async ctrlArrowUp(locator?: Locator | null) {
|
||||
await this.ctrlSend('ArrowUp', locator)
|
||||
}
|
||||
|
||||
async ctrlArrowDown() {
|
||||
await this.ctrlSend('ArrowDown')
|
||||
async ctrlArrowDown(locator?: Locator | null) {
|
||||
await this.ctrlSend('ArrowDown', locator)
|
||||
}
|
||||
|
||||
async closeMenu() {
|
||||
|
||||
@@ -13,35 +13,46 @@ export class Topbar {
|
||||
await this.page.locator('.p-menubar-mobile .p-menubar-button').click()
|
||||
}
|
||||
|
||||
async getMenuItem(itemLabel: string): Promise<Locator> {
|
||||
getMenuItem(itemLabel: string): Locator {
|
||||
return this.page.locator(`.p-menubar-item-label:text-is("${itemLabel}")`)
|
||||
}
|
||||
|
||||
async getWorkflowTab(tabName: string): Promise<Locator> {
|
||||
getWorkflowTab(tabName: string): Locator {
|
||||
return this.page
|
||||
.locator(`.workflow-tabs .workflow-label:has-text("${tabName}")`)
|
||||
.locator('..')
|
||||
}
|
||||
|
||||
async closeWorkflowTab(tabName: string) {
|
||||
const tab = await this.getWorkflowTab(tabName)
|
||||
const tab = this.getWorkflowTab(tabName)
|
||||
await tab.locator('.close-button').click({ force: true })
|
||||
}
|
||||
|
||||
async saveWorkflow(workflowName: string) {
|
||||
await this.triggerTopbarCommand(['Workflow', 'Save'])
|
||||
await this.page.locator('.p-dialog-content input').fill(workflowName)
|
||||
await this.page.keyboard.press('Enter')
|
||||
// Wait for the dialog to close.
|
||||
await this.page.waitForTimeout(300)
|
||||
getSaveDialog(): Locator {
|
||||
return this.page.locator('.p-dialog-content input')
|
||||
}
|
||||
|
||||
async saveWorkflowAs(workflowName: string) {
|
||||
await this.triggerTopbarCommand(['Workflow', 'Save As'])
|
||||
await this.page.locator('.p-dialog-content input').fill(workflowName)
|
||||
saveWorkflow(workflowName: string): Promise<void> {
|
||||
return this._saveWorkflow(workflowName, 'Save')
|
||||
}
|
||||
|
||||
saveWorkflowAs(workflowName: string): Promise<void> {
|
||||
return this._saveWorkflow(workflowName, 'Save As')
|
||||
}
|
||||
|
||||
async _saveWorkflow(workflowName: string, command: 'Save' | 'Save As') {
|
||||
await this.triggerTopbarCommand(['Workflow', command])
|
||||
await this.getSaveDialog().fill(workflowName)
|
||||
await this.page.keyboard.press('Enter')
|
||||
|
||||
// Wait for workflow service to finish saving
|
||||
await this.page.waitForFunction(
|
||||
() => !window['app'].extensionManager.workflow.isBusy,
|
||||
undefined,
|
||||
{ timeout: 3000 }
|
||||
)
|
||||
// Wait for the dialog to close.
|
||||
await this.page.waitForTimeout(300)
|
||||
await this.getSaveDialog().waitFor({ state: 'hidden', timeout: 500 })
|
||||
}
|
||||
|
||||
async triggerTopbarCommand(path: string[]) {
|
||||
|
||||
@@ -285,7 +285,8 @@ test.describe('Node Interaction', () => {
|
||||
position: {
|
||||
x: 50,
|
||||
y: 10
|
||||
}
|
||||
},
|
||||
delay: 5
|
||||
})
|
||||
await comfyPage.page.keyboard.type('Hello World')
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
@@ -300,7 +301,8 @@ test.describe('Node Interaction', () => {
|
||||
position: {
|
||||
x: 50,
|
||||
y: 50
|
||||
}
|
||||
},
|
||||
delay: 5
|
||||
})
|
||||
expect(await comfyPage.page.locator('.node-title-editor').count()).toBe(0)
|
||||
})
|
||||
@@ -352,7 +354,8 @@ test.describe('Group Interaction', () => {
|
||||
position: {
|
||||
x: 50,
|
||||
y: 10
|
||||
}
|
||||
},
|
||||
delay: 5
|
||||
})
|
||||
await comfyPage.page.keyboard.type('Hello World')
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
@@ -504,7 +507,7 @@ test.describe('Widget Interaction', () => {
|
||||
await expect(textBox).toHaveValue('')
|
||||
await textBox.fill('Hello World')
|
||||
await expect(textBox).toHaveValue('Hello World')
|
||||
await comfyPage.ctrlZ()
|
||||
await comfyPage.ctrlZ(null)
|
||||
await expect(textBox).toHaveValue('')
|
||||
})
|
||||
|
||||
@@ -515,9 +518,9 @@ test.describe('Widget Interaction', () => {
|
||||
await textBox.fill('1girl')
|
||||
await expect(textBox).toHaveValue('1girl')
|
||||
await textBox.selectText()
|
||||
await comfyPage.ctrlArrowUp()
|
||||
await comfyPage.ctrlArrowUp(null)
|
||||
await expect(textBox).toHaveValue('(1girl:1.05)')
|
||||
await comfyPage.ctrlZ()
|
||||
await comfyPage.ctrlZ(null)
|
||||
await expect(textBox).toHaveValue('1girl')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -504,6 +504,7 @@ test.describe('Menu', () => {
|
||||
await comfyPage.menu.topbar.saveWorkflow(workflowName)
|
||||
expect(await comfyPage.menu.topbar.getTabNames()).toEqual([workflowName])
|
||||
await comfyPage.menu.topbar.closeWorkflowTab(workflowName)
|
||||
await comfyPage.nextFrame()
|
||||
expect(await comfyPage.menu.topbar.getTabNames()).toEqual([
|
||||
'Unsaved Workflow'
|
||||
])
|
||||
@@ -525,8 +526,7 @@ test.describe('Menu', () => {
|
||||
})
|
||||
|
||||
test('Displays keybinding next to item', async ({ comfyPage }) => {
|
||||
const workflowMenuItem =
|
||||
await comfyPage.menu.topbar.getMenuItem('Workflow')
|
||||
const workflowMenuItem = comfyPage.menu.topbar.getMenuItem('Workflow')
|
||||
await workflowMenuItem.click()
|
||||
const exportTag = comfyPage.page.locator('.keybinding-tag', {
|
||||
hasText: 'Ctrl + s'
|
||||
|
||||
@@ -15,7 +15,7 @@ test.describe('Node search box', () => {
|
||||
|
||||
test(`Can trigger on group body double click`, async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('single_group_only')
|
||||
await comfyPage.page.mouse.dblclick(50, 50)
|
||||
await comfyPage.page.mouse.dblclick(50, 50, { delay: 5 })
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1)
|
||||
})
|
||||
|
||||
70
browser_tests/nodeTemplate.spec.ts
Normal file
70
browser_tests/nodeTemplate.spec.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
comfyPageFixture as test,
|
||||
comfyExpect as expect
|
||||
} from './fixtures/ComfyPage'
|
||||
|
||||
// Old `nodeTemplate.ts` system
|
||||
test.describe('Node Template', () => {
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.clearNodeTemplates()
|
||||
})
|
||||
|
||||
test('Can create and use node template', async ({ comfyPage }) => {
|
||||
const templateName = 'Can create node template template'
|
||||
|
||||
await comfyPage.clearNodeTemplates()
|
||||
await comfyPage.reload()
|
||||
|
||||
// TODO: Flaky test. Right click requires delay after reload, but other interactions do not.
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
|
||||
// Enter filename when prompt dialog shown
|
||||
comfyPage.page.on('dialog', (dialog) => dialog.accept(templateName))
|
||||
|
||||
// Ctrl + drag over 3 nodes
|
||||
await comfyPage.dragAndDrop(
|
||||
{ x: 175, y: 252 },
|
||||
{ x: 483, y: 564 },
|
||||
'ControlOrMeta'
|
||||
)
|
||||
expect(await comfyPage.getGraphSelectedItemsCount()).toEqual(3)
|
||||
|
||||
await comfyPage.rightClickCanvas()
|
||||
await comfyPage.clickContextMenuItem('Save Selected as Template')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.rightClickCanvas()
|
||||
await comfyPage.clickContextMenuItem('Node Templates >')
|
||||
await comfyPage.clickContextMenuItem(templateName)
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot()
|
||||
})
|
||||
|
||||
test('Can load old format template', async ({ comfyPage }) => {
|
||||
await comfyPage.setNodeTemplates('vintage_clipboard_template.json')
|
||||
await comfyPage.reload()
|
||||
|
||||
// TODO: Flaky test. Right click requires delay after reload, but other interactions do not.
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
|
||||
await comfyPage.rightClickCanvas()
|
||||
await comfyPage.clickContextMenuItem('Node Templates >')
|
||||
await comfyPage.clickContextMenuItem('vintageClipboard Template')
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot()
|
||||
})
|
||||
|
||||
test('Can load new format template', async ({ comfyPage }) => {
|
||||
await comfyPage.setNodeTemplates('node_template_templates.json')
|
||||
await comfyPage.reload()
|
||||
|
||||
// TODO: Flaky test. Right click requires delay after reload, but other interactions do not.
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
|
||||
await comfyPage.rightClickCanvas()
|
||||
await comfyPage.clickContextMenuItem('Node Templates >')
|
||||
await comfyPage.clickContextMenuItem('Three Nodes Template')
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot()
|
||||
})
|
||||
})
|
||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "comfyui-frontend",
|
||||
"version": "1.3.41",
|
||||
"version": "1.3.43",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "comfyui-frontend",
|
||||
"version": "1.3.41",
|
||||
"version": "1.3.43",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.2.10",
|
||||
"@comfyorg/litegraph": "^0.8.25",
|
||||
"@comfyorg/comfyui-electron-types": "^0.2.16",
|
||||
"@comfyorg/litegraph": "^0.8.26",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
@@ -1916,15 +1916,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@comfyorg/comfyui-electron-types": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.2.10.tgz",
|
||||
"integrity": "sha512-JwqFeqmJBp6n276Ki+VEkMkO43rFHobdt93AzJYpWC+BXGUuvTyquon/MvblWtJDnTdO0mGWGXztDFe0sXie6A==",
|
||||
"version": "0.2.16",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.2.16.tgz",
|
||||
"integrity": "sha512-Hm6NeyMK4sd2V5AyOnvfI+tvCsXr5NBG8wOZlWyyD17ADpbQnpm6qPMWzvm4vCp/YvTR7cUbDGiY0quhofuQGg==",
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.8.25",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.25.tgz",
|
||||
"integrity": "sha512-VYMpxNLAwLgmT1mFX77RNA3O5KavhWBmYJpb3+BLW6BwmnDCd0QHX9gy5IFsGSpQP28k2lWgANIcGZF2Ev2eqg==",
|
||||
"version": "0.8.26",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.26.tgz",
|
||||
"integrity": "sha512-q0Vcd5usphR5nghfyFksVx+VM+eSB1MyX8Ne304KFDnr214KQMA6DAjrEQJlGBUUCybLiOtPCvd3dxPecEQiSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.3.41",
|
||||
"version": "1.3.43",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -72,8 +72,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.2.10",
|
||||
"@comfyorg/litegraph": "^0.8.25",
|
||||
"@comfyorg/comfyui-electron-types": "^0.2.16",
|
||||
"@comfyorg/litegraph": "^0.8.26",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
|
||||
@@ -83,7 +83,10 @@ body {
|
||||
/* Position at the first row */
|
||||
grid-row: 1;
|
||||
/* Top menu bar dropdown needs to be above of graph canvas splitter overlay which is z-index: 999 */
|
||||
z-index: 1000;
|
||||
/* Top menu bar z-index needs to be higher than bottom menu bar z-index as by default
|
||||
pysssss's image feed is located at body-bottom, and it can overlap with the queue button, which
|
||||
is located in body-top. */
|
||||
z-index: 1001;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
:disabled="props.error"
|
||||
@click="triggerCancelDownload"
|
||||
icon="pi pi-times-circle"
|
||||
severity="danger"
|
||||
v-tooltip.top="t('electronFileDownload.cancel')"
|
||||
/>
|
||||
</div>
|
||||
@@ -73,6 +74,7 @@ import { ref, computed } from 'vue'
|
||||
import { formatSize } from '@/utils/formatUtil'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
|
||||
|
||||
const props = defineProps<{
|
||||
url: string
|
||||
@@ -81,55 +83,37 @@ const props = defineProps<{
|
||||
error?: string
|
||||
}>()
|
||||
|
||||
interface ModelDownload {
|
||||
url: string
|
||||
status: 'paused' | 'in_progress' | 'cancelled'
|
||||
progress: number
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const { DownloadManager } = electronAPI()
|
||||
const label = computed(() => props.label || props.url.split('/').pop())
|
||||
const hint = computed(() => props.hint || props.url)
|
||||
const download = useDownload(props.url)
|
||||
const status = ref<ModelDownload | null>(null)
|
||||
const downloadProgress = ref<number>(0)
|
||||
const status = ref<string | null>(null)
|
||||
const fileSize = computed(() =>
|
||||
download.fileSize.value ? formatSize(download.fileSize.value) : '?'
|
||||
)
|
||||
const electronDownloadStore = useElectronDownloadStore()
|
||||
const [savePath, filename] = props.label.split('/')
|
||||
|
||||
const downloads: ModelDownload[] = await DownloadManager.getAllDownloads()
|
||||
const modelDownload = downloads.find(({ url }) => url === props.url)
|
||||
electronDownloadStore.$subscribe((mutation, { downloads }) => {
|
||||
const download = downloads.find((download) => props.url === download.url)
|
||||
|
||||
const updateProperties = (download: ModelDownload) => {
|
||||
if (download.url === props.url) {
|
||||
if (download) {
|
||||
downloadProgress.value = Number((download.progress * 100).toFixed(1))
|
||||
status.value = download.status
|
||||
downloadProgress.value = (download.progress * 100).toFixed(1)
|
||||
}
|
||||
}
|
||||
|
||||
DownloadManager.onDownloadProgress((data: ModelDownload) => {
|
||||
updateProperties(data)
|
||||
})
|
||||
|
||||
const triggerDownload = async () => {
|
||||
await DownloadManager.startDownload(
|
||||
props.url,
|
||||
filename.trim(),
|
||||
savePath.trim()
|
||||
)
|
||||
await electronDownloadStore.start({
|
||||
url: props.url,
|
||||
savePath: savePath.trim(),
|
||||
filename: filename.trim()
|
||||
})
|
||||
}
|
||||
|
||||
const triggerCancelDownload = async () => {
|
||||
await DownloadManager.cancelDownload(props.url)
|
||||
}
|
||||
|
||||
const triggerPauseDownload = async () => {
|
||||
await DownloadManager.pauseDownload(props.url)
|
||||
}
|
||||
|
||||
const triggerResumeDownload = async () => {
|
||||
await DownloadManager.resumeDownload(props.url)
|
||||
}
|
||||
const triggerCancelDownload = () => electronDownloadStore.cancel(props.url)
|
||||
const triggerPauseDownload = () => electronDownloadStore.pause(props.url)
|
||||
const triggerResumeDownload = () => electronDownloadStore.resume(props.url)
|
||||
</script>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</ScrollPanel>
|
||||
<Divider layout="vertical" class="mx-1 2xl:mx-4" />
|
||||
<ScrollPanel class="settings-content flex-grow">
|
||||
<Tabs :value="tabValue">
|
||||
<Tabs :value="tabValue" :lazy="true">
|
||||
<FirstTimeUIMessage v-if="tabValue === 'Comfy'" />
|
||||
<TabPanels class="settings-tab-panels">
|
||||
<TabPanel key="search-results" value="Search Results">
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<teleport to=".graph-canvas-container">
|
||||
<!-- Load splitter overlay only after comfyApp is ready. -->
|
||||
<!-- If load immediately, the top-level splitter stateKey won't be correctly
|
||||
synced with the stateStorage (localStorage). -->
|
||||
<LiteGraphCanvasSplitterOverlay
|
||||
v-if="betaMenuEnabled && !workspaceStore.focusMode"
|
||||
v-if="comfyAppReady && betaMenuEnabled && !workspaceStore.focusMode"
|
||||
>
|
||||
<template #side-bar-panel>
|
||||
<SideToolbar />
|
||||
@@ -104,6 +107,12 @@ watchEffect(() => {
|
||||
)
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
LiteGraph.middle_click_slot_add_default_node = settingStore.get(
|
||||
'Comfy.Node.MiddleClickRerouteNode'
|
||||
)
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
nodeDefStore.showDeprecated = settingStore.get('Comfy.Node.ShowDeprecated')
|
||||
})
|
||||
@@ -134,6 +143,24 @@ watchEffect(() => {
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
const linkMarkerShape = settingStore.get('Comfy.Graph.LinkMarkers')
|
||||
const { canvas } = canvasStore
|
||||
if (canvas) {
|
||||
canvas.linkMarkerShape = linkMarkerShape
|
||||
canvas.setDirty(false, true)
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
const reroutesEnabled = settingStore.get('Comfy.RerouteBeta')
|
||||
const { canvas } = canvasStore
|
||||
if (canvas) {
|
||||
canvas.reroutesEnabled = reroutesEnabled
|
||||
canvas.setDirty(false, true)
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (!canvasStore.canvas) return
|
||||
|
||||
@@ -213,6 +240,7 @@ usePragmaticDroppable(() => canvasRef.value, {
|
||||
}
|
||||
})
|
||||
|
||||
const comfyAppReady = ref(false)
|
||||
onMounted(async () => {
|
||||
// Backward compatible
|
||||
// Assign all properties of lg to window
|
||||
@@ -239,6 +267,7 @@ onMounted(async () => {
|
||||
window['app'] = comfyApp
|
||||
window['graph'] = comfyApp.graph
|
||||
|
||||
comfyAppReady.value = true
|
||||
emit('ready')
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -77,8 +77,7 @@ const pathError = defineModel<string>('pathError', { required: true })
|
||||
const appData = ref('')
|
||||
const appPath = ref('')
|
||||
|
||||
// TODO: Implement the actual electron API.
|
||||
const electron = electronAPI() as any
|
||||
const electron = electronAPI()
|
||||
|
||||
// Get system paths on component mount
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -68,38 +68,45 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watchEffect } from 'vue'
|
||||
import { ref, computed, watchEffect } from 'vue'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Button from 'primevue/button'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import Message from 'primevue/message'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { MigrationItems } from '@comfyorg/comfyui-electron-types'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const electron = electronAPI() as any
|
||||
const electron = electronAPI()
|
||||
|
||||
const sourcePath = defineModel<string>('sourcePath', { required: false })
|
||||
const migrationItemIds = defineModel<string[]>('migrationItemIds', {
|
||||
required: false
|
||||
})
|
||||
|
||||
const migrationItems = ref([])
|
||||
const migrationItems = ref(
|
||||
MigrationItems.map((item) => ({
|
||||
...item,
|
||||
selected: true
|
||||
}))
|
||||
)
|
||||
|
||||
const pathError = ref('')
|
||||
const isValidSource = computed(
|
||||
() => sourcePath.value !== '' && pathError.value === ''
|
||||
)
|
||||
|
||||
const validateSource = async () => {
|
||||
if (!sourcePath.value) {
|
||||
const validateSource = async (sourcePath: string) => {
|
||||
if (!sourcePath) {
|
||||
pathError.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
pathError.value = ''
|
||||
const validation = await electron.validateComfyUISource(sourcePath.value)
|
||||
const validation = await electron.validateComfyUISource(sourcePath)
|
||||
|
||||
if (!validation.isValid) pathError.value = validation.error
|
||||
} catch (error) {
|
||||
@@ -113,7 +120,7 @@ const browsePath = async () => {
|
||||
const result = await electron.showDirectoryPicker()
|
||||
if (result) {
|
||||
sourcePath.value = result
|
||||
await validateSource()
|
||||
await validateSource(result)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -121,13 +128,6 @@ const browsePath = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
migrationItems.value = (await electron.migrationItems()).map((item) => ({
|
||||
...item,
|
||||
selected: true
|
||||
}))
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
migrationItemIds.value = migrationItems.value
|
||||
.filter((item) => item.selected)
|
||||
|
||||
@@ -58,7 +58,6 @@ const getNewNodeLocation = (): [number, number] => {
|
||||
}
|
||||
|
||||
const originalEvent = triggerEvent.value.detail.originalEvent
|
||||
// @ts-expect-error LiteGraph types are not typed
|
||||
return [originalEvent.canvasX, originalEvent.canvasY]
|
||||
}
|
||||
const nodeFilters = ref<FilterAndValue[]>([])
|
||||
@@ -153,8 +152,16 @@ const showContextMenu = (e: LiteGraphCanvasEvent) => {
|
||||
showSearchBox: () => showSearchBox(e)
|
||||
}
|
||||
const connectionOptions = firstLink.output
|
||||
? { nodeFrom: firstLink.node, slotFrom: firstLink.output }
|
||||
: { nodeTo: firstLink.node, slotTo: firstLink.input }
|
||||
? {
|
||||
nodeFrom: firstLink.node,
|
||||
slotFrom: firstLink.output,
|
||||
afterRerouteId: firstLink.afterRerouteId
|
||||
}
|
||||
: {
|
||||
nodeTo: firstLink.node,
|
||||
slotTo: firstLink.input,
|
||||
afterRerouteId: firstLink.afterRerouteId
|
||||
}
|
||||
canvasStore.canvas.showConnectionMenu({
|
||||
...connectionOptions,
|
||||
...commonOptions
|
||||
@@ -178,7 +185,6 @@ const canvasEventHandler = (e: LiteGraphCanvasEvent) => {
|
||||
} else if (e.detail.subType === 'group-double-click') {
|
||||
const group = e.detail.group
|
||||
const [x, y] = group.pos
|
||||
// @ts-expect-error LiteGraphCanvasEvent is not typed
|
||||
const relativeY = e.detail.originalEvent.canvasY - y
|
||||
// Show search box if the click is NOT on the title bar
|
||||
if (relativeY > group.titleHeight) {
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<ElectronDownloadItems v-if="isElectron()" />
|
||||
|
||||
<TreeExplorer
|
||||
class="model-lib-tree-explorer py-0"
|
||||
:roots="renderedRoot.children"
|
||||
@@ -48,6 +50,7 @@ import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import TreeExplorer from '@/components/common/TreeExplorer.vue'
|
||||
import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue'
|
||||
import ModelTreeLeaf from '@/components/sidebar/tabs/modelLibrary/ModelTreeLeaf.vue'
|
||||
import ElectronDownloadItems from '@/components/sidebar/tabs/modelLibrary/ElectronDownloadItems.vue'
|
||||
import {
|
||||
ComfyModelDef,
|
||||
ModelFolder,
|
||||
@@ -65,6 +68,8 @@ import { computed, ref, watch, toRef, onMounted, nextTick } from 'vue'
|
||||
import type { TreeNode } from 'primevue/treenode'
|
||||
import { app } from '@/scripts/app'
|
||||
import { buildTree } from '@/utils/treeUtil'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
const modelStore = useModelStore()
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
const settingStore = useSettingStore()
|
||||
@@ -164,6 +169,7 @@ const renderedRoot = computed<TreeExplorerNode<ModelOrFolder>>(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fillNodeInfo(root.value)
|
||||
})
|
||||
|
||||
|
||||
97
src/components/sidebar/tabs/modelLibrary/DownloadItem.vue
Normal file
97
src/components/sidebar/tabs/modelLibrary/DownloadItem.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
{{ getDownloadLabel(download.savePath) }}
|
||||
</div>
|
||||
<div v-if="['cancelled', 'error'].includes(download.status)">
|
||||
<Chip
|
||||
class="h-6 text-sm font-light bg-red-700 mt-2"
|
||||
removable
|
||||
@remove="handleRemoveDownload"
|
||||
>
|
||||
{{ t('electronFileDownload.cancelled') }}
|
||||
</Chip>
|
||||
</div>
|
||||
<div
|
||||
class="mt-2 flex flex-row items-center gap-2"
|
||||
v-if="['in_progress', 'paused', 'completed'].includes(download.status)"
|
||||
>
|
||||
<ProgressBar
|
||||
class="flex-1"
|
||||
:value="Number((download.progress * 100).toFixed(1))"
|
||||
/>
|
||||
|
||||
<Button
|
||||
class="file-action-button w-[22px] h-[22px]"
|
||||
size="small"
|
||||
rounded
|
||||
@click="triggerPauseDownload"
|
||||
v-if="download.status === 'in_progress'"
|
||||
icon="pi pi-pause"
|
||||
v-tooltip.top="t('electronFileDownload.pause')"
|
||||
/>
|
||||
|
||||
<Button
|
||||
class="file-action-button w-[22px] h-[22px]"
|
||||
size="small"
|
||||
rounded
|
||||
@click="triggerResumeDownload"
|
||||
v-if="download.status === 'paused'"
|
||||
icon="pi pi-play"
|
||||
v-tooltip.top="t('electronFileDownload.resume')"
|
||||
/>
|
||||
|
||||
<Button
|
||||
class="file-action-button w-[22px] h-[22px] p-red"
|
||||
size="small"
|
||||
rounded
|
||||
severity="danger"
|
||||
@click="triggerCancelDownload"
|
||||
v-if="['in_progress', 'paused'].includes(download.status)"
|
||||
icon="pi pi-times-circle"
|
||||
v-tooltip.top="t('electronFileDownload.cancel')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import ProgressBar from 'primevue/progressbar'
|
||||
import Chip from 'primevue/chip'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { t } = useI18n()
|
||||
import {
|
||||
type ElectronDownload,
|
||||
useElectronDownloadStore
|
||||
} from '@/stores/electronDownloadStore'
|
||||
const electronDownloadStore = useElectronDownloadStore()
|
||||
|
||||
const props = defineProps<{
|
||||
download: ElectronDownload
|
||||
}>()
|
||||
|
||||
const getDownloadLabel = (savePath: string, filename: string) => {
|
||||
let parts = (savePath ?? '').split('/')
|
||||
parts = parts.length === 1 ? parts[0].split('\\') : parts
|
||||
const name = parts.pop()
|
||||
const dir = parts.pop()
|
||||
return `${dir}/${name}`
|
||||
}
|
||||
|
||||
const triggerCancelDownload = () =>
|
||||
electronDownloadStore.cancel(props.download.url)
|
||||
const triggerPauseDownload = () =>
|
||||
electronDownloadStore.pause(props.download.url)
|
||||
const triggerResumeDownload = () =>
|
||||
electronDownloadStore.resume(props.download.url)
|
||||
|
||||
const handleRemoveDownload = () => {
|
||||
electronDownloadStore.$patch((state) => {
|
||||
state.downloads = state.downloads.filter(
|
||||
({ url }) => url !== props.download.url
|
||||
)
|
||||
state.hasChanged = true
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="mx-6 mb-4" v-if="downloads.length > 0">
|
||||
<div class="text-lg my-4">
|
||||
{{ $t('electronFileDownload.inProgress') }}
|
||||
</div>
|
||||
|
||||
<template v-for="download in downloads" :key="download.url">
|
||||
<DownloadItem :download="download" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import DownloadItem from './DownloadItem.vue'
|
||||
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const electronDownloadStore = useElectronDownloadStore()
|
||||
const { downloads } = storeToRefs(electronDownloadStore)
|
||||
</script>
|
||||
@@ -3,12 +3,16 @@ import { app } from '../../scripts/app'
|
||||
import { api } from '../../scripts/api'
|
||||
import { mergeIfValid } from './widgetInputs'
|
||||
import { ManageGroupDialog } from './groupNodeManage'
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
||||
import { LGraphCanvas, LiteGraph, type LGraph } from '@comfyorg/litegraph'
|
||||
import { LGraphNode, type NodeId } from '@comfyorg/litegraph/dist/LGraphNode'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { ComfyLink, ComfyNode, ComfyWorkflowJSON } from '@/types/comfyWorkflow'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { ComfyExtension } from '@/types/comfy'
|
||||
import {
|
||||
deserialiseAndCreate,
|
||||
serialise
|
||||
} from '@/extensions/core/vintageClipboard'
|
||||
|
||||
type GroupNodeWorkflowData = {
|
||||
external: ComfyLink[]
|
||||
@@ -144,21 +148,15 @@ class GroupNodeBuilder {
|
||||
}
|
||||
|
||||
// Use the built in copyToClipboard function to generate the node data we need
|
||||
const backup = localStorage.getItem('litegrapheditor_clipboard')
|
||||
try {
|
||||
// @ts-expect-error
|
||||
// TODO Figure out if copyToClipboard is really taking this param
|
||||
app.canvas.copyToClipboard(this.nodes)
|
||||
const config = JSON.parse(
|
||||
localStorage.getItem('litegrapheditor_clipboard')
|
||||
)
|
||||
const serialised = serialise(this.nodes, app.canvas.graph)
|
||||
const config = JSON.parse(serialised)
|
||||
|
||||
storeLinkTypes(config)
|
||||
storeExternalLinks(config)
|
||||
|
||||
return config
|
||||
} finally {
|
||||
localStorage.setItem('litegrapheditor_clipboard', backup)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -842,7 +840,6 @@ export class GroupNodeHandler {
|
||||
|
||||
this.node.convertToNodes = () => {
|
||||
const addInnerNodes = () => {
|
||||
const backup = localStorage.getItem('litegrapheditor_clipboard')
|
||||
// Clone the node data so we dont mutate it for other nodes
|
||||
const c = { ...this.groupData.nodeData }
|
||||
c.nodes = [...c.nodes]
|
||||
@@ -858,9 +855,7 @@ export class GroupNodeHandler {
|
||||
}
|
||||
c.nodes[i] = { ...c.nodes[i], id }
|
||||
}
|
||||
localStorage.setItem('litegrapheditor_clipboard', JSON.stringify(c))
|
||||
app.canvas.pasteFromClipboard()
|
||||
localStorage.setItem('litegrapheditor_clipboard', backup)
|
||||
deserialiseAndCreate(JSON.stringify(c), app.canvas)
|
||||
|
||||
const [x, y] = this.node.pos
|
||||
let top
|
||||
@@ -923,10 +918,8 @@ export class GroupNodeHandler {
|
||||
|
||||
// Shift each node
|
||||
for (const newNode of newNodes) {
|
||||
newNode.pos = [
|
||||
newNode.pos[0] - (left - x),
|
||||
newNode.pos[1] - (top - y)
|
||||
]
|
||||
newNode.pos[0] -= left - x
|
||||
newNode.pos[1] -= top - y
|
||||
}
|
||||
|
||||
return { newNodes, selectedIds }
|
||||
@@ -970,11 +963,15 @@ export class GroupNodeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
app.canvas.emitBeforeChange()
|
||||
|
||||
const { newNodes, selectedIds } = addInnerNodes()
|
||||
reconnectInputs(selectedIds)
|
||||
reconnectOutputs(selectedIds)
|
||||
app.graph.remove(this.node)
|
||||
|
||||
app.canvas.emitAfterChange()
|
||||
|
||||
return newNodes
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ComfyDialog, $el } from '../../scripts/ui'
|
||||
import { GroupNodeConfig, GroupNodeHandler } from './groupNode'
|
||||
import { LGraphCanvas } from '@comfyorg/litegraph'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { deserialiseAndCreate } from '@/extensions/core/vintageClipboard'
|
||||
|
||||
// Adds the ability to save and add multiple nodes as a template
|
||||
// To save:
|
||||
@@ -414,8 +415,14 @@ app.registerExtension({
|
||||
clipboardAction(async () => {
|
||||
const data = JSON.parse(t.data)
|
||||
await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {})
|
||||
localStorage.setItem('litegrapheditor_clipboard', t.data)
|
||||
app.canvas.pasteFromClipboard()
|
||||
|
||||
// Check for old clipboard format
|
||||
if (!data.reroutes) {
|
||||
deserialiseAndCreate(t.data, app.canvas)
|
||||
} else {
|
||||
localStorage.setItem('litegrapheditor_clipboard', t.data)
|
||||
app.canvas.pasteFromClipboard()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ let touchCount = 0
|
||||
app.registerExtension({
|
||||
name: 'Comfy.SimpleTouchSupport',
|
||||
setup() {
|
||||
let zoomPos
|
||||
let touchDist
|
||||
let touchTime
|
||||
let lastTouch
|
||||
|
||||
let lastScale
|
||||
function getMultiTouchPos(e) {
|
||||
return Math.hypot(
|
||||
e.touches[0].clientX - e.touches[1].clientX,
|
||||
@@ -19,11 +19,19 @@ app.registerExtension({
|
||||
)
|
||||
}
|
||||
|
||||
app.canvasEl.addEventListener(
|
||||
function getMultiTouchCenter(e) {
|
||||
return {
|
||||
clientX: (e.touches[0].clientX + e.touches[1].clientX) / 2,
|
||||
clientY: (e.touches[0].clientY + e.touches[1].clientY) / 2
|
||||
}
|
||||
}
|
||||
|
||||
app.canvasEl.parentElement.addEventListener(
|
||||
'touchstart',
|
||||
(e) => {
|
||||
(e: TouchEvent) => {
|
||||
touchCount++
|
||||
lastTouch = null
|
||||
lastScale = null
|
||||
if (e.touches?.length === 1) {
|
||||
// Store start time for press+hold for context menu
|
||||
touchTime = new Date()
|
||||
@@ -32,7 +40,10 @@ app.registerExtension({
|
||||
touchTime = null
|
||||
if (e.touches?.length === 2) {
|
||||
// Store center pos for zoom
|
||||
zoomPos = getMultiTouchPos(e)
|
||||
lastScale = app.canvas.ds.scale
|
||||
lastTouch = getMultiTouchCenter(e)
|
||||
|
||||
touchDist = getMultiTouchPos(e)
|
||||
app.canvas.pointer_is_down = false
|
||||
}
|
||||
}
|
||||
@@ -40,54 +51,80 @@ app.registerExtension({
|
||||
true
|
||||
)
|
||||
|
||||
app.canvasEl.addEventListener('touchend', (e: TouchEvent) => {
|
||||
touchZooming = false
|
||||
touchCount = e.touches?.length ?? touchCount - 1
|
||||
app.canvasEl.parentElement.addEventListener('touchend', (e: TouchEvent) => {
|
||||
touchCount--
|
||||
|
||||
if (e.touches?.length !== 1) touchZooming = false
|
||||
if (touchTime && !e.touches?.length) {
|
||||
if (new Date().getTime() - touchTime > 600) {
|
||||
try {
|
||||
// hack to get litegraph to use this event
|
||||
e.constructor = CustomEvent
|
||||
} catch (error) {}
|
||||
// @ts-expect-error
|
||||
e.clientX = lastTouch.clientX
|
||||
// @ts-expect-error
|
||||
e.clientY = lastTouch.clientY
|
||||
|
||||
app.canvas.pointer_is_down = true
|
||||
// @ts-expect-error
|
||||
app.canvas._mousedown_callback(e)
|
||||
if (e.target === app.canvasEl) {
|
||||
app.canvasEl.dispatchEvent(
|
||||
new PointerEvent('pointerdown', {
|
||||
button: 2,
|
||||
clientX: e.changedTouches[0].clientX,
|
||||
clientY: e.changedTouches[0].clientY
|
||||
})
|
||||
)
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
touchTime = null
|
||||
}
|
||||
})
|
||||
|
||||
app.canvasEl.addEventListener(
|
||||
app.canvasEl.parentElement.addEventListener(
|
||||
'touchmove',
|
||||
(e) => {
|
||||
touchTime = null
|
||||
if (e.touches?.length === 2) {
|
||||
if (e.touches?.length === 2 && lastTouch && !e.ctrlKey && !e.shiftKey) {
|
||||
e.preventDefault() // Prevent browser from zooming when two textareas are touched
|
||||
app.canvas.pointer_is_down = false
|
||||
touchZooming = true
|
||||
// @ts-expect-error
|
||||
LiteGraph.closeAllContextMenus()
|
||||
|
||||
LiteGraph.closeAllContextMenus(window)
|
||||
// @ts-expect-error
|
||||
app.canvas.search_box?.close()
|
||||
const newZoomPos = getMultiTouchPos(e)
|
||||
const newTouchDist = getMultiTouchPos(e)
|
||||
|
||||
const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2
|
||||
const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2
|
||||
const center = getMultiTouchCenter(e)
|
||||
|
||||
let scale = app.canvas.ds.scale
|
||||
const diff = zoomPos - newZoomPos
|
||||
if (diff > 0.5) {
|
||||
scale *= 1 / 1.07
|
||||
} else if (diff < -0.5) {
|
||||
scale *= 1.07
|
||||
let scale = (lastScale * newTouchDist) / touchDist
|
||||
|
||||
const newX = (center.clientX - lastTouch.clientX) / scale
|
||||
const newY = (center.clientY - lastTouch.clientY) / scale
|
||||
|
||||
// Code from LiteGraph
|
||||
if (scale < app.canvas.ds.min_scale) {
|
||||
scale = app.canvas.ds.min_scale
|
||||
} else if (scale > app.canvas.ds.max_scale) {
|
||||
scale = app.canvas.ds.max_scale
|
||||
}
|
||||
app.canvas.ds.changeScale(scale, [midX, midY])
|
||||
|
||||
const oldScale = app.canvas.ds.scale
|
||||
|
||||
app.canvas.ds.scale = scale
|
||||
|
||||
// Code from LiteGraph
|
||||
if (Math.abs(app.canvas.ds.scale - 1) < 0.01) {
|
||||
app.canvas.ds.scale = 1
|
||||
}
|
||||
|
||||
const newScale = app.canvas.ds.scale
|
||||
|
||||
const convertScaleToOffset = (scale) => [
|
||||
center.clientX / scale - app.canvas.ds.offset[0],
|
||||
center.clientY / scale - app.canvas.ds.offset[1]
|
||||
]
|
||||
var oldCenter = convertScaleToOffset(oldScale)
|
||||
var newCenter = convertScaleToOffset(newScale)
|
||||
|
||||
app.canvas.ds.offset[0] += newX + newCenter[0] - oldCenter[0]
|
||||
app.canvas.ds.offset[1] += newY + newCenter[1] - oldCenter[1]
|
||||
|
||||
lastTouch.clientX = center.clientX
|
||||
lastTouch.clientY = center.clientY
|
||||
|
||||
app.canvas.setDirty(true, true)
|
||||
zoomPos = newZoomPos
|
||||
}
|
||||
},
|
||||
true
|
||||
@@ -100,6 +137,7 @@ LGraphCanvas.prototype.processMouseDown = function (e) {
|
||||
if (touchZooming || touchCount) {
|
||||
return
|
||||
}
|
||||
app.canvas.pointer_is_down = false // Prevent context menu from opening on second tap
|
||||
return processMouseDown.apply(this, arguments)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { IWidget } from '@comfyorg/litegraph'
|
||||
import type { DOMWidget } from '@/scripts/domWidget'
|
||||
import { ComfyNodeDef } from '@/types/apiTypes'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { Widgets } from '@/types/comfy'
|
||||
|
||||
type FolderType = 'input' | 'output' | 'temp'
|
||||
|
||||
@@ -37,7 +36,7 @@ function getResourceURL(
|
||||
|
||||
async function uploadFile(
|
||||
audioWidget: IWidget,
|
||||
audioUIWidget: DOMWidget<HTMLAudioElement>,
|
||||
audioUIWidget: DOMWidget<HTMLAudioElement, string>,
|
||||
file: File,
|
||||
updateNode: boolean,
|
||||
pasted: boolean = false
|
||||
@@ -95,12 +94,10 @@ app.registerExtension({
|
||||
audio.classList.add('comfy-audio')
|
||||
audio.setAttribute('name', 'media')
|
||||
|
||||
const audioUIWidget: DOMWidget<HTMLAudioElement> = node.addDOMWidget(
|
||||
inputName,
|
||||
/* name=*/ 'audioUI',
|
||||
audio,
|
||||
{ serialize: false }
|
||||
)
|
||||
const audioUIWidget: DOMWidget<HTMLAudioElement, string> =
|
||||
node.addDOMWidget(inputName, /* name=*/ 'audioUI', audio, {
|
||||
serialize: false
|
||||
})
|
||||
|
||||
const isOutputNode = node.constructor.nodeData.output_node
|
||||
if (isOutputNode) {
|
||||
@@ -121,7 +118,7 @@ app.registerExtension({
|
||||
}
|
||||
return { widget: audioUIWidget }
|
||||
}
|
||||
} as Widgets
|
||||
}
|
||||
},
|
||||
onNodeOutputsUpdated(nodeOutputs: Record<number, any>) {
|
||||
for (const [nodeId, output] of Object.entries(nodeOutputs)) {
|
||||
@@ -129,7 +126,7 @@ app.registerExtension({
|
||||
if ('audio' in output) {
|
||||
const audioUIWidget = node.widgets.find(
|
||||
(w) => w.name === 'audioUI'
|
||||
) as unknown as DOMWidget<HTMLAudioElement>
|
||||
) as unknown as DOMWidget<HTMLAudioElement, string>
|
||||
const audio = output.audio[0]
|
||||
audioUIWidget.element.src = api.apiURL(
|
||||
getResourceURL(audio.subfolder, audio.filename, audio.type)
|
||||
@@ -156,7 +153,7 @@ app.registerExtension({
|
||||
)
|
||||
const audioUIWidget = node.widgets.find(
|
||||
(w: IWidget) => w.name === 'audioUI'
|
||||
) as DOMWidget<HTMLAudioElement>
|
||||
) as unknown as DOMWidget<HTMLAudioElement, string>
|
||||
|
||||
const onAudioWidgetUpdate = () => {
|
||||
audioUIWidget.element.src = api.apiURL(
|
||||
|
||||
119
src/extensions/core/vintageClipboard.ts
Normal file
119
src/extensions/core/vintageClipboard.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
// @ts-strict-ignore
|
||||
import type { LGraph, LGraphCanvas, LGraphNode } from '@comfyorg/litegraph'
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
|
||||
/**
|
||||
* Serialises an array of nodes using a modified version of the old Litegraph copy (& paste) function
|
||||
* @param nodes All nodes to be serialised
|
||||
* @param graph The graph we are working in
|
||||
* @returns A serialised string of all nodes, and their connections
|
||||
* @deprecated Format not in use anywhere else.
|
||||
*/
|
||||
export function serialise(nodes: LGraphNode[], graph: LGraph): string {
|
||||
const serialisable = {
|
||||
nodes: [],
|
||||
links: []
|
||||
}
|
||||
let index = 0
|
||||
const cloneable: LGraphNode[] = []
|
||||
|
||||
for (const node of nodes) {
|
||||
if (node.clonable === false) continue
|
||||
|
||||
node._relative_id = index++
|
||||
cloneable.push(node)
|
||||
}
|
||||
|
||||
// Clone the node
|
||||
for (const node of cloneable) {
|
||||
const cloned = node.clone()
|
||||
if (!cloned) {
|
||||
console.warn('node type not found: ' + node.type)
|
||||
continue
|
||||
}
|
||||
|
||||
serialisable.nodes.push(cloned.serialize())
|
||||
if (!node.inputs?.length) continue
|
||||
|
||||
// For inputs only, gather link details of every connection
|
||||
for (const input of node.inputs) {
|
||||
if (!input || input.link == null) continue
|
||||
|
||||
const link = graph.links.get(input.link)
|
||||
if (!link) continue
|
||||
|
||||
const outNode = graph.getNodeById(link.origin_id)
|
||||
if (!outNode) continue
|
||||
|
||||
// Special format for old Litegraph copy & paste only
|
||||
serialisable.links.push([
|
||||
outNode._relative_id,
|
||||
link.origin_slot,
|
||||
node._relative_id,
|
||||
link.target_slot,
|
||||
outNode.id
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(serialisable)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialises nodes and links using a modified version of the old Litegraph (copy &) paste function
|
||||
* @param data The serialised nodes and links to create
|
||||
* @param canvas The canvas to create the serialised items in
|
||||
*/
|
||||
export function deserialiseAndCreate(data: string, canvas: LGraphCanvas): void {
|
||||
if (!data) return
|
||||
|
||||
const { graph, graph_mouse } = canvas
|
||||
canvas.emitBeforeChange()
|
||||
graph.beforeChange()
|
||||
|
||||
const deserialised = JSON.parse(data)
|
||||
|
||||
// Find the top left point of the boundary of all pasted nodes
|
||||
const topLeft = [Infinity, Infinity]
|
||||
for (const { pos } of deserialised.nodes) {
|
||||
if (topLeft[0] > pos[0]) topLeft[0] = pos[0]
|
||||
if (topLeft[1] > pos[1]) topLeft[1] = pos[1]
|
||||
}
|
||||
|
||||
// Silent default instead of throw
|
||||
if (!Number.isFinite(topLeft[0]) || !Number.isFinite(topLeft[1])) {
|
||||
topLeft[0] = graph_mouse[0]
|
||||
topLeft[1] = graph_mouse[1]
|
||||
}
|
||||
|
||||
// Create nodes
|
||||
const nodes: LGraphNode[] = []
|
||||
for (const info of deserialised.nodes) {
|
||||
const node = LiteGraph.createNode(info.type)
|
||||
if (!node) continue
|
||||
|
||||
node.configure(info)
|
||||
|
||||
// Paste to the bottom right of pointer
|
||||
node.pos[0] += graph_mouse[0] - topLeft[0]
|
||||
node.pos[1] += graph_mouse[1] - topLeft[1]
|
||||
|
||||
graph.add(node, true)
|
||||
nodes.push(node)
|
||||
}
|
||||
|
||||
// Create links
|
||||
for (const info of deserialised.links) {
|
||||
const relativeId = info[0]
|
||||
const outNode = relativeId != null ? nodes[relativeId] : undefined
|
||||
|
||||
const inNode = nodes[info[2]]
|
||||
if (outNode && inNode) outNode.connect(info[1], inNode, info[3])
|
||||
else console.warn('Warning, nodes missing on pasting')
|
||||
}
|
||||
|
||||
canvas.selectNodes(nodes)
|
||||
|
||||
graph.afterChange()
|
||||
canvas.emitAfterChange()
|
||||
}
|
||||
@@ -119,6 +119,7 @@ const messages = {
|
||||
sortOrder: 'Sort Order'
|
||||
},
|
||||
modelLibrary: 'Model Library',
|
||||
downloads: 'Downloads',
|
||||
queueTab: {
|
||||
showFlatList: 'Show Flat List',
|
||||
backToAllTasks: 'Back to All Tasks',
|
||||
@@ -170,9 +171,12 @@ const messages = {
|
||||
toggleLinkVisibility: 'Toggle Link Visibility'
|
||||
},
|
||||
electronFileDownload: {
|
||||
inProgress: 'In Progress',
|
||||
pause: 'Pause Download',
|
||||
paused: 'Paused',
|
||||
resume: 'Resume Download',
|
||||
cancel: 'Cancel Download'
|
||||
cancel: 'Cancel Download',
|
||||
cancelled: 'Cancelled'
|
||||
}
|
||||
},
|
||||
zh: {
|
||||
|
||||
@@ -22,7 +22,12 @@ const guardElectronAccess = (
|
||||
}
|
||||
|
||||
const router = createRouter({
|
||||
history: isFileProtocol() ? createWebHashHistory() : createWebHistory(),
|
||||
history: isFileProtocol()
|
||||
? createWebHashHistory()
|
||||
: // Base path must be specified to ensure correct relative paths
|
||||
// Example: For URL 'http://localhost:7801/ComfyBackendDirect',
|
||||
// we need this base path or assets will incorrectly resolve from 'http://localhost:7801/'
|
||||
createWebHistory(window.location.pathname),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
importA1111,
|
||||
getLatentMetadata
|
||||
} from './pnginfo'
|
||||
import { addDomClippingSetting } from './domWidget'
|
||||
import { createImageHost, calculateImageGrid } from './ui/imagePreview'
|
||||
import { DraggableList } from './ui/draggableList'
|
||||
import { applyTextReplacements, addStylesheet } from './utils'
|
||||
@@ -65,6 +64,7 @@ import { shallowReactive } from 'vue'
|
||||
import { type IBaseWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
import { workflowService } from '@/services/workflowService'
|
||||
import { useWidgetStore } from '@/stores/widgetStore'
|
||||
import { deserialiseAndCreate } from '@/extensions/core/vintageClipboard'
|
||||
|
||||
export const ANIM_PREVIEW_WIDGET = '$$comfy_animation_preview'
|
||||
|
||||
@@ -1806,7 +1806,6 @@ export class ComfyApp {
|
||||
])
|
||||
await this.#loadExtensions()
|
||||
|
||||
addDomClippingSetting()
|
||||
this.#addProcessMouseHandler()
|
||||
this.#addProcessKeyHandler()
|
||||
this.#addConfigureHandler()
|
||||
@@ -2125,8 +2124,14 @@ export class ComfyApp {
|
||||
continue
|
||||
}
|
||||
|
||||
localStorage.setItem('litegrapheditor_clipboard', template.data)
|
||||
app.canvas.pasteFromClipboard()
|
||||
// Check for old clipboard format
|
||||
const data = JSON.parse(template.data)
|
||||
if (!data.reroutes) {
|
||||
deserialiseAndCreate(template.data, app.canvas)
|
||||
} else {
|
||||
localStorage.setItem('litegrapheditor_clipboard', template.data)
|
||||
app.canvas.pasteFromClipboard()
|
||||
}
|
||||
|
||||
// Move mouse position down to paste the next template below
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
// @ts-strict-ignore
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { app, ANIM_PREVIEW_WIDGET } from './app'
|
||||
import { LGraphCanvas, LGraphNode, LiteGraph } from '@comfyorg/litegraph'
|
||||
import type { Vector4 } from '@comfyorg/litegraph'
|
||||
import {
|
||||
ICustomWidget,
|
||||
IWidgetOptions
|
||||
} from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
const SIZE = Symbol()
|
||||
|
||||
@@ -12,15 +17,20 @@ interface Rect {
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface DOMWidget<T = HTMLElement> {
|
||||
type: string
|
||||
export interface DOMWidget<T extends HTMLElement, V extends object | string>
|
||||
extends ICustomWidget<T> {
|
||||
// All unrecognized types will be treated the same way as 'custom' in litegraph internally.
|
||||
type: 'custom'
|
||||
name: string
|
||||
computedHeight?: number
|
||||
element?: T
|
||||
options: any
|
||||
value?: any
|
||||
options: DOMWidgetOptions<T, V>
|
||||
value: V
|
||||
y?: number
|
||||
callback?: (value: any) => void
|
||||
callback?: (value: V) => void
|
||||
/**
|
||||
* Draw the widget on the canvas.
|
||||
*/
|
||||
draw?: (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
node: LGraphNode,
|
||||
@@ -28,9 +38,31 @@ export interface DOMWidget<T = HTMLElement> {
|
||||
y: number,
|
||||
widgetHeight: number
|
||||
) => void
|
||||
/**
|
||||
* TODO(huchenlei): Investigate when is this callback fired. `onRemove` is
|
||||
* on litegraph's IBaseWidget definition, but not called in litegraph.
|
||||
* Currently only called in widgetInputs.ts.
|
||||
*/
|
||||
onRemove?: () => void
|
||||
}
|
||||
|
||||
export interface DOMWidgetOptions<
|
||||
T extends HTMLElement,
|
||||
V extends object | string
|
||||
> extends IWidgetOptions {
|
||||
hideOnZoom?: boolean
|
||||
selectOn?: string[]
|
||||
onHide?: (widget: DOMWidget<T, V>) => void
|
||||
getValue?: () => V
|
||||
setValue?: (value: V) => void
|
||||
getMinHeight?: () => number
|
||||
getMaxHeight?: () => number
|
||||
getHeight?: () => string | number
|
||||
onDraw?: (widget: DOMWidget<T, V>) => void
|
||||
beforeResize?: (this: DOMWidget<T, V>, node: LGraphNode) => void
|
||||
afterResize?: (this: DOMWidget<T, V>, node: LGraphNode) => void
|
||||
}
|
||||
|
||||
function intersect(a: Rect, b: Rect): Vector4 | null {
|
||||
const x = Math.max(a.x, b.x)
|
||||
const num1 = Math.min(a.x + a.width, b.x + b.width)
|
||||
@@ -248,27 +280,15 @@ LGraphCanvas.prototype.computeVisibleNodes = function (): LGraphNode[] {
|
||||
return visibleNodes
|
||||
}
|
||||
|
||||
let enableDomClipping = true
|
||||
|
||||
export function addDomClippingSetting(): void {
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.DOMClippingEnabled',
|
||||
category: ['Comfy', 'Node', 'DOMClippingEnabled'],
|
||||
name: 'Enable DOM element clipping (enabling may reduce performance)',
|
||||
type: 'boolean',
|
||||
defaultValue: enableDomClipping,
|
||||
onChange(value) {
|
||||
enableDomClipping = !!value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
LGraphNode.prototype.addDOMWidget = function (
|
||||
LGraphNode.prototype.addDOMWidget = function <
|
||||
T extends HTMLElement,
|
||||
V extends object | string
|
||||
>(
|
||||
name: string,
|
||||
type: string,
|
||||
element: HTMLElement,
|
||||
options: Record<string, any> = {}
|
||||
): DOMWidget {
|
||||
element: T,
|
||||
options: DOMWidgetOptions<T, V> = {}
|
||||
): DOMWidget<T, V> {
|
||||
options = { hideOnZoom: true, selectOn: ['focus', 'click'], ...options }
|
||||
|
||||
if (!element.parentElement) {
|
||||
@@ -294,13 +314,15 @@ LGraphNode.prototype.addDOMWidget = function (
|
||||
element.title = tooltip
|
||||
}
|
||||
|
||||
const widget: DOMWidget = {
|
||||
const widget: DOMWidget<T, V> = {
|
||||
// @ts-expect-error All unrecognized types will be treated the same way as 'custom'
|
||||
// in litegraph internally.
|
||||
type,
|
||||
name,
|
||||
get value() {
|
||||
get value(): V {
|
||||
return options.getValue?.() ?? undefined
|
||||
},
|
||||
set value(v) {
|
||||
set value(v: V) {
|
||||
options.setValue?.(v)
|
||||
widget.callback?.(widget.value)
|
||||
},
|
||||
@@ -320,8 +342,11 @@ LGraphNode.prototype.addDOMWidget = function (
|
||||
const hidden =
|
||||
(!!options.hideOnZoom && scale < 0.5) ||
|
||||
widget.computedHeight <= 0 ||
|
||||
// @ts-expect-error Used by widgetInputs.ts
|
||||
widget.type === 'converted-widget' ||
|
||||
// @ts-expect-error Used by groupNode.ts
|
||||
widget.type === 'hidden'
|
||||
|
||||
element.dataset.shouldHide = hidden ? 'true' : 'false'
|
||||
const isInVisibleNodes = element.dataset.isInVisibleNodes === 'true'
|
||||
const isCollapsed = element.dataset.collapsed === 'true'
|
||||
@@ -353,7 +378,7 @@ LGraphNode.prototype.addDOMWidget = function (
|
||||
pointerEvents: app.canvas.read_only ? 'none' : 'auto'
|
||||
})
|
||||
|
||||
if (enableDomClipping) {
|
||||
if (useSettingStore().get('Comfy.DOMClippingEnabled')) {
|
||||
element.style.clipPath = getClipPath(node, element, elRect)
|
||||
element.style.willChange = 'clip-path'
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
LinkReleaseTriggerMode
|
||||
} from '@/types/searchBoxTypes'
|
||||
import type { SettingParams } from '@/types/settingTypes'
|
||||
import { LinkMarkerShape } from '@comfyorg/litegraph'
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
|
||||
export const CORE_SETTINGS: SettingParams[] = [
|
||||
@@ -484,5 +485,41 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.3.40'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Node.MiddleClickRerouteNode',
|
||||
name: 'Middle-click creates a new Reroute node',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.3.42'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.RerouteBeta',
|
||||
name: 'Opt-in to the reroute beta test',
|
||||
tooltip:
|
||||
'Enables the new native reroutes.\n\nReroutes can be added by holding alt and dragging from a link line, or on the link menu.\n\nDisabling this option is non-destructive - reroutes are hidden.',
|
||||
experimental: true,
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
versionAdded: '1.3.42'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.LinkMarkers',
|
||||
name: 'Link midpoint markers',
|
||||
defaultValue: LinkMarkerShape.Circle,
|
||||
type: 'combo',
|
||||
options: [
|
||||
{ value: LinkMarkerShape.None, text: 'None' },
|
||||
{ value: LinkMarkerShape.Circle, text: 'Circle' },
|
||||
{ value: LinkMarkerShape.Arrow, text: 'Arrow' }
|
||||
],
|
||||
versionAdded: '1.3.42'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.DOMClippingEnabled',
|
||||
category: ['Comfy', 'Node', 'DOMClippingEnabled'],
|
||||
name: 'Enable DOM element clipping (enabling may reduce performance)',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
}
|
||||
]
|
||||
|
||||
69
src/stores/electronDownloadStore.ts
Normal file
69
src/stores/electronDownloadStore.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { isElectron, electronAPI } from '@/utils/envUtil'
|
||||
|
||||
export interface ElectronDownload {
|
||||
url: string
|
||||
status: 'paused' | 'in_progress' | 'cancelled'
|
||||
progress: number
|
||||
savePath: string
|
||||
filename: string
|
||||
}
|
||||
|
||||
/** Electron donwloads store handler */
|
||||
export const useElectronDownloadStore = defineStore('downloads', () => {
|
||||
const downloads = ref<ElectronDownload[]>([])
|
||||
const { DownloadManager } = electronAPI()
|
||||
|
||||
const findByUrl = (url: string) =>
|
||||
downloads.value.find((download) => url === download.url)
|
||||
|
||||
const initialize = async () => {
|
||||
if (isElectron()) {
|
||||
const allDownloads: ElectronDownload[] =
|
||||
(await DownloadManager.getAllDownloads()) as unknown as ElectronDownload[]
|
||||
|
||||
for (const download of allDownloads) {
|
||||
downloads.value.push(download)
|
||||
}
|
||||
|
||||
// ToDO: replace with ElectronDownload type
|
||||
DownloadManager.onDownloadProgress((data: any) => {
|
||||
if (!findByUrl(data.url)) {
|
||||
downloads.value.push(data)
|
||||
}
|
||||
|
||||
const download = findByUrl(data.url)
|
||||
|
||||
if (download) {
|
||||
download.progress = data.progress
|
||||
download.status = data.status
|
||||
download.filename = data.filename
|
||||
download.savePath = data.savePath
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
void initialize()
|
||||
|
||||
const start = ({
|
||||
url,
|
||||
savePath,
|
||||
filename
|
||||
}: Pick<ElectronDownload, 'url' | 'savePath' | 'filename'>) =>
|
||||
DownloadManager.startDownload(url, savePath, filename)
|
||||
const pause = (url: string) => DownloadManager.pauseDownload(url)
|
||||
const resume = (url: string) => DownloadManager.resumeDownload(url)
|
||||
const cancel = (url: string) => DownloadManager.cancelDownload(url)
|
||||
|
||||
return {
|
||||
downloads,
|
||||
start,
|
||||
pause,
|
||||
resume,
|
||||
cancel,
|
||||
findByUrl,
|
||||
initialize
|
||||
}
|
||||
})
|
||||
@@ -284,32 +284,45 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
workflows.value.filter((workflow) => workflow.isModified)
|
||||
)
|
||||
|
||||
/** A filesystem operation is currently in progress (e.g. save, rename, delete) */
|
||||
const isBusy = ref<boolean>(false)
|
||||
|
||||
const renameWorkflow = async (workflow: ComfyWorkflow, newPath: string) => {
|
||||
// Capture all needed values upfront
|
||||
const oldPath = workflow.path
|
||||
const wasBookmarked = bookmarkStore.isBookmarked(oldPath)
|
||||
|
||||
const openIndex = detachWorkflow(workflow)
|
||||
// Perform the actual rename operation first
|
||||
isBusy.value = true
|
||||
try {
|
||||
await workflow.rename(newPath)
|
||||
} finally {
|
||||
attachWorkflow(workflow, openIndex)
|
||||
}
|
||||
// Capture all needed values upfront
|
||||
const oldPath = workflow.path
|
||||
const wasBookmarked = bookmarkStore.isBookmarked(oldPath)
|
||||
|
||||
// Update bookmarks
|
||||
if (wasBookmarked) {
|
||||
bookmarkStore.setBookmarked(oldPath, false)
|
||||
bookmarkStore.setBookmarked(newPath, true)
|
||||
const openIndex = detachWorkflow(workflow)
|
||||
// Perform the actual rename operation first
|
||||
try {
|
||||
await workflow.rename(newPath)
|
||||
} finally {
|
||||
attachWorkflow(workflow, openIndex)
|
||||
}
|
||||
|
||||
// Update bookmarks
|
||||
if (wasBookmarked) {
|
||||
bookmarkStore.setBookmarked(oldPath, false)
|
||||
bookmarkStore.setBookmarked(newPath, true)
|
||||
}
|
||||
} finally {
|
||||
isBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deleteWorkflow = async (workflow: ComfyWorkflow) => {
|
||||
await workflow.delete()
|
||||
if (bookmarkStore.isBookmarked(workflow.path)) {
|
||||
bookmarkStore.setBookmarked(workflow.path, false)
|
||||
isBusy.value = true
|
||||
try {
|
||||
await workflow.delete()
|
||||
if (bookmarkStore.isBookmarked(workflow.path)) {
|
||||
bookmarkStore.setBookmarked(workflow.path, false)
|
||||
}
|
||||
delete workflowLookup.value[workflow.path]
|
||||
} finally {
|
||||
isBusy.value = false
|
||||
}
|
||||
delete workflowLookup.value[workflow.path]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -317,12 +330,17 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
* @param workflow The workflow to save.
|
||||
*/
|
||||
const saveWorkflow = async (workflow: ComfyWorkflow) => {
|
||||
// Detach the workflow and re-attach to force refresh the tree objects.
|
||||
const openIndex = detachWorkflow(workflow)
|
||||
isBusy.value = true
|
||||
try {
|
||||
await workflow.save()
|
||||
// Detach the workflow and re-attach to force refresh the tree objects.
|
||||
const openIndex = detachWorkflow(workflow)
|
||||
try {
|
||||
await workflow.save()
|
||||
} finally {
|
||||
attachWorkflow(workflow, openIndex)
|
||||
}
|
||||
} finally {
|
||||
attachWorkflow(workflow, openIndex)
|
||||
isBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,6 +351,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
openedWorkflowIndexShift,
|
||||
openWorkflow,
|
||||
isOpen,
|
||||
isBusy,
|
||||
closeWorkflow,
|
||||
createTemporary,
|
||||
renameWorkflow,
|
||||
|
||||
@@ -1,35 +1,24 @@
|
||||
// @ts-strict-ignore
|
||||
import type {
|
||||
ConnectingLink,
|
||||
LGraphNode,
|
||||
Vector2,
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
INodeSlot
|
||||
INodeSlot,
|
||||
ISlotType
|
||||
} from '@comfyorg/litegraph'
|
||||
import type { ISlotType } from '@comfyorg/litegraph'
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import { RerouteId } from '@comfyorg/litegraph/dist/Reroute'
|
||||
|
||||
export class ConnectingLinkImpl implements ConnectingLink {
|
||||
node: LGraphNode
|
||||
slot: number
|
||||
input: INodeInputSlot | null
|
||||
output: INodeOutputSlot | null
|
||||
pos: Vector2
|
||||
|
||||
constructor(
|
||||
node: LGraphNode,
|
||||
slot: number,
|
||||
input: INodeInputSlot | null,
|
||||
output: INodeOutputSlot | null,
|
||||
pos: Vector2
|
||||
) {
|
||||
this.node = node
|
||||
this.slot = slot
|
||||
this.input = input
|
||||
this.output = output
|
||||
this.pos = pos
|
||||
}
|
||||
public node: LGraphNode,
|
||||
public slot: number,
|
||||
public input: INodeInputSlot | undefined,
|
||||
public output: INodeOutputSlot | undefined,
|
||||
public pos: Vector2,
|
||||
public afterRerouteId?: RerouteId
|
||||
) {}
|
||||
|
||||
static createFromPlainObject(obj: ConnectingLink) {
|
||||
return new ConnectingLinkImpl(
|
||||
@@ -37,12 +26,13 @@ export class ConnectingLinkImpl implements ConnectingLink {
|
||||
obj.slot,
|
||||
obj.input,
|
||||
obj.output,
|
||||
obj.pos
|
||||
obj.pos,
|
||||
obj.afterRerouteId
|
||||
)
|
||||
}
|
||||
|
||||
get type(): ISlotType | null {
|
||||
const result = this.input ? this.input.type : this.output.type
|
||||
const result = this.input ? this.input.type : this.output?.type ?? null
|
||||
return result === -1 ? null : result
|
||||
}
|
||||
|
||||
@@ -71,9 +61,9 @@ export class ConnectingLinkImpl implements ConnectingLink {
|
||||
}
|
||||
|
||||
if (this.releaseSlotType === 'input') {
|
||||
this.node.connect(this.slot, newNode, newNodeSlot)
|
||||
this.node.connect(this.slot, newNode, newNodeSlot, this.afterRerouteId)
|
||||
} else {
|
||||
newNode.connect(newNodeSlot, this.node, this.slot)
|
||||
newNode.connect(newNodeSlot, this.node, this.slot, this.afterRerouteId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,8 @@ import InstallLocationPicker from '@/components/install/InstallLocationPicker.vu
|
||||
import MigrationPicker from '@/components/install/MigrationPicker.vue'
|
||||
import DesktopSettingsConfiguration from '@/components/install/DesktopSettingsConfiguration.vue'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, toRaw } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const installPath = ref('')
|
||||
const pathError = ref('')
|
||||
@@ -100,14 +101,17 @@ const allowMetrics = ref(true)
|
||||
|
||||
const hasError = computed(() => pathError.value !== '')
|
||||
|
||||
const router = useRouter()
|
||||
const install = () => {
|
||||
;(electronAPI() as any).installComfyUI({
|
||||
const options = toRaw({
|
||||
installPath: installPath.value,
|
||||
autoUpdate: autoUpdate.value,
|
||||
allowMetrics: allowMetrics.value,
|
||||
migrationSourcePath: migrationSourcePath.value,
|
||||
migrationItemIds: migrationItemIds.value
|
||||
migrationItemIds: toRaw(migrationItemIds.value)
|
||||
})
|
||||
electronAPI().installComfyUI(options)
|
||||
router.push('/server-start')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
ProgressStatus,
|
||||
ProgressMessages
|
||||
} from '@comfyorg/comfyui-electron-types'
|
||||
import { electronAPI as getElectronAPI } from '@/utils/envUtil'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
|
||||
const electronAPI = getElectronAPI()
|
||||
const electron = electronAPI()
|
||||
|
||||
const status = ref<ProgressStatus>(ProgressStatus.INITIAL_STATE)
|
||||
const logs = ref<string[]>([])
|
||||
@@ -35,9 +35,9 @@ const fetchLogs = async () => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
electronAPI.sendReady()
|
||||
electronAPI.onProgressUpdate(updateProgress)
|
||||
electronAPI.onLogMessage((message: string) => {
|
||||
electron.sendReady()
|
||||
electron.onProgressUpdate(updateProgress)
|
||||
electron.onLogMessage((message: string) => {
|
||||
addLogMessage(message)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -94,6 +94,7 @@ const DEV_SERVER_COMFYUI_URL = process.env.DEV_SERVER_COMFYUI_URL || 'http://127
|
||||
export default defineConfig({
|
||||
base: '',
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/internal': {
|
||||
target: DEV_SERVER_COMFYUI_URL,
|
||||
@@ -175,6 +176,9 @@ export default defineConfig({
|
||||
},
|
||||
|
||||
optimizeDeps: {
|
||||
exclude: ['@comfyorg/litegraph']
|
||||
exclude: [
|
||||
'@comfyorg/litegraph',
|
||||
'@comfyorg/comfyui-electron-types'
|
||||
]
|
||||
}
|
||||
}) as UserConfigExport
|
||||
|
||||
Reference in New Issue
Block a user