mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-19 14:30:07 +00:00
Merge remote-tracking branch 'origin/main' into feature/show_dropped_image_in_output_node
This commit is contained in:
@@ -12,6 +12,10 @@ DEV_SERVER_COMFYUI_URL=http://127.0.0.1:8188
|
||||
# to ComfyUI launch script to serve the custom web version.
|
||||
DEPLOY_COMFYUI_DIR=/home/ComfyUI/web
|
||||
|
||||
# The directory containing the ComfyUI installation used to run Playwright tests.
|
||||
# If you aren't using a separate install for testing, point this to your regular install.
|
||||
TEST_COMFYUI_DIR=/home/ComfyUI
|
||||
|
||||
# The directory containing the ComfyUI_examples repo used to extract test workflows.
|
||||
EXAMPLE_REPO_PATH=tests-ui/ComfyUI_examples
|
||||
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"trailingComma": "none"
|
||||
"trailingComma": "none",
|
||||
"printWidth": 80
|
||||
}
|
||||
54
README.md
54
README.md
@@ -33,11 +33,11 @@
|
||||
|
||||
### Nightly Release
|
||||
|
||||
Nightly releases are published daily at [https://github.com/Comfy-Org/ComfyUI_frontend/releases](https://github.com/Comfy-Org/ComfyUI_frontend/releases).
|
||||
Nightly releases are published daily at [https://github.com/Comfy-Org/ComfyUI_frontend/releases](https://github.com/Comfy-Org/ComfyUI_frontend/releases).
|
||||
|
||||
To use the latest nightly release, add the following command line argument to your ComfyUI launch script:
|
||||
|
||||
```
|
||||
```bat
|
||||
--front-end-version Comfy-Org/ComfyUI_frontend@latest
|
||||
```
|
||||
|
||||
@@ -62,6 +62,23 @@ There will be a 2-day feature freeze before each stable release. During this per
|
||||
|
||||
### Major features
|
||||
|
||||
<details>
|
||||
<summary>v1.3.7: Keybinding customization</summary>
|
||||
|
||||
## Basic UI
|
||||

|
||||
|
||||
## Reset button
|
||||

|
||||
|
||||
## Edit Keybinding
|
||||

|
||||

|
||||
|
||||
[rec.webm](https://github.com/user-attachments/assets/a3984ed9-eb28-4d47-86c0-7fc3efc2b5d0)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.2.4: Node library sidebar tab</summary>
|
||||
|
||||
@@ -90,6 +107,32 @@ https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf
|
||||
|
||||
### QoL changes
|
||||
|
||||
<details>
|
||||
<summary>v1.3.6: **Litegraph** Toggle link visibility</summary>
|
||||
|
||||
[rec.webm](https://github.com/user-attachments/assets/34e460ac-fbbc-44ef-bfbb-99a84c2ae2be)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.3.4: **Litegraph** Auto widget to input conversion</summary>
|
||||
|
||||
Dropping a link of correct type on node widget will automatically convert the widget to input.
|
||||
|
||||
[rec.webm](https://github.com/user-attachments/assets/15cea0b0-b225-4bec-af50-2cdb16dc46bf)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.3.4: **Litegraph** Canvas pan mode</summary>
|
||||
|
||||
The canvas becomes readonly in pan mode. Pan mode is activated by clicking the pan mode button on the canvas menu
|
||||
or by holding the space key.
|
||||
|
||||
[rec.webm](https://github.com/user-attachments/assets/c7872532-a2ac-44c1-9e7d-9e03b5d1a80b)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.3.1: **Litegraph** Shift drag link to create a new link</summary>
|
||||
|
||||
@@ -97,6 +140,13 @@ https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.2.62: **Litegraph** Show optional input slots as donuts</summary>
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.2.44: **Litegraph** Double click group title to edit</summary>
|
||||
|
||||
|
||||
@@ -575,6 +575,10 @@ export class ComfyPage {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async getVisibleToastCount() {
|
||||
return await this.page.locator('.p-toast:visible').count()
|
||||
}
|
||||
|
||||
async clickTextEncodeNode1() {
|
||||
await this.canvas.click({
|
||||
position: {
|
||||
|
||||
@@ -5,7 +5,7 @@ This document outlines the setup and usage of Playwright for testing the ComfyUI
|
||||
## WARNING
|
||||
|
||||
The browser tests will change the ComfyUI backend state, such as user settings and saved workflows.
|
||||
Please backup your ComfyUI data before running the tests locally.
|
||||
If `TEST_COMFYUI_DIR` in `.env` isn't set to your `(Comfy Path)/ComfyUI` directory, these changes won't be automatically restored.
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
128
browser_tests/assets/old_workflow_converted_input.json
Normal file
128
browser_tests/assets/old_workflow_converted_input.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"last_node_id": 2,
|
||||
"last_link_id": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "ControlNetApplyAdvanced",
|
||||
"pos": {
|
||||
"0": 449,
|
||||
"1": 204
|
||||
},
|
||||
"size": [
|
||||
340.20001220703125,
|
||||
166
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "control_net",
|
||||
"type": "CONTROL_NET",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "strength",
|
||||
"type": "FLOAT",
|
||||
"link": 1,
|
||||
"widget": {
|
||||
"name": "strength"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ControlNetApplyAdvanced"
|
||||
},
|
||||
"widgets_values": [
|
||||
1,
|
||||
0,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "PrimitiveNode",
|
||||
"pos": {
|
||||
"0": 177,
|
||||
"1": 265
|
||||
},
|
||||
"size": [
|
||||
210,
|
||||
82
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "FLOAT",
|
||||
"type": "FLOAT",
|
||||
"links": [
|
||||
1
|
||||
],
|
||||
"widget": {
|
||||
"name": "strength"
|
||||
}
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Run widget replace on values": false
|
||||
},
|
||||
"widgets_values": [
|
||||
1,
|
||||
"fixed"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
4,
|
||||
"FLOAT"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": {
|
||||
"0": 47.541666666666515,
|
||||
"1": 186.9375
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -95,3 +95,34 @@ test.describe('Missing models warning', () => {
|
||||
await expect(folderSelect).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Settings', () => {
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
// Restore default setting value
|
||||
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.1)
|
||||
})
|
||||
|
||||
test('@mobile Should be visible on mobile', async ({ comfyPage }) => {
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const searchBox = comfyPage.page.locator('.settings-content')
|
||||
await expect(searchBox).toBeVisible()
|
||||
})
|
||||
|
||||
test('Can open settings with hotkey', async ({ comfyPage }) => {
|
||||
await comfyPage.page.keyboard.down('ControlOrMeta')
|
||||
await comfyPage.page.keyboard.press(',')
|
||||
await comfyPage.page.keyboard.up('ControlOrMeta')
|
||||
const settingsLocator = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsLocator).toBeVisible()
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await expect(settingsLocator).not.toBeVisible()
|
||||
})
|
||||
|
||||
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 () => {
|
||||
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
20
browser_tests/globalSetup.ts
Normal file
20
browser_tests/globalSetup.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { FullConfig } from '@playwright/test'
|
||||
import { backupPath } from './utils/backupUtils'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
export default function globalSetup(config: FullConfig) {
|
||||
if (!process.env.CI) {
|
||||
if (process.env.TEST_COMFYUI_DIR) {
|
||||
backupPath([process.env.TEST_COMFYUI_DIR, 'user'])
|
||||
backupPath([process.env.TEST_COMFYUI_DIR, 'models'], {
|
||||
renameAndReplaceWithScaffolding: true
|
||||
})
|
||||
} else {
|
||||
console.warn(
|
||||
'Set TEST_COMFYUI_DIR in .env to prevent user data (settings, workflows, etc.) from being overwritten'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
12
browser_tests/globalTeardown.ts
Normal file
12
browser_tests/globalTeardown.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { FullConfig } from '@playwright/test'
|
||||
import { restorePath } from './utils/backupUtils'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
export default function globalTeardown(config: FullConfig) {
|
||||
if (!process.env.CI && process.env.TEST_COMFYUI_DIR) {
|
||||
restorePath([process.env.TEST_COMFYUI_DIR, 'user'])
|
||||
restorePath([process.env.TEST_COMFYUI_DIR, 'models'])
|
||||
}
|
||||
}
|
||||
@@ -252,4 +252,20 @@ test.describe('Group Node', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Keybindings', () => {
|
||||
test('Convert to group node, no selection', async ({ comfyPage }) => {
|
||||
expect(await comfyPage.getVisibleToastCount()).toBe(0)
|
||||
await comfyPage.page.keyboard.press('Alt+g')
|
||||
await comfyPage.page.waitForTimeout(300)
|
||||
expect(await comfyPage.getVisibleToastCount()).toBe(1)
|
||||
})
|
||||
test('Convert to group node, selected 1 node', async ({ comfyPage }) => {
|
||||
expect(await comfyPage.getVisibleToastCount()).toBe(0)
|
||||
await comfyPage.clickTextEncodeNode1()
|
||||
await comfyPage.page.keyboard.press('Alt+g')
|
||||
await comfyPage.page.waitForTimeout(300)
|
||||
expect(await comfyPage.getVisibleToastCount()).toBe(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,9 +12,19 @@ test.describe('Node Interaction', () => {
|
||||
})
|
||||
|
||||
test.describe('Node Selection', () => {
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
// Deselect all nodes
|
||||
await comfyPage.clickEmptySpace()
|
||||
const multiSelectModifiers = ['Control', 'Shift', 'Meta']
|
||||
|
||||
multiSelectModifiers.forEach((modifier) => {
|
||||
test(`Can add multiple nodes to selection using ${modifier}+Click`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
|
||||
for (const node of clipNodes) {
|
||||
await node.click('title', { modifiers: [modifier] })
|
||||
}
|
||||
const selectedNodeCount = await comfyPage.getSelectedGraphNodesCount()
|
||||
expect(selectedNodeCount).toBe(clipNodes.length)
|
||||
})
|
||||
})
|
||||
|
||||
test('Can highlight selected', async ({ comfyPage }) => {
|
||||
@@ -497,15 +507,3 @@ test.describe('Load duplicate workflow', () => {
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Menu interactions', () => {
|
||||
test('Can open settings with hotkey', async ({ comfyPage }) => {
|
||||
await comfyPage.page.keyboard.down('ControlOrMeta')
|
||||
await comfyPage.page.keyboard.press(',')
|
||||
await comfyPage.page.keyboard.up('ControlOrMeta')
|
||||
const settingsLocator = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsLocator).toBeVisible()
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await expect(settingsLocator).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -416,10 +416,9 @@ test.describe('Menu', () => {
|
||||
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
await tab.open()
|
||||
expect(await tab.getTopLevelSavedWorkflowNames()).toEqual([
|
||||
'workflow1.json',
|
||||
'workflow2.json'
|
||||
])
|
||||
expect(await tab.getTopLevelSavedWorkflowNames()).toEqual(
|
||||
expect.arrayContaining(['workflow1.json', 'workflow2.json'])
|
||||
)
|
||||
})
|
||||
|
||||
test('Does not report warning when switching between opened workflows', async ({
|
||||
@@ -525,17 +524,4 @@ test.describe('Menu', () => {
|
||||
expect(await comfyPage.getSetting('Comfy.UseNewMenu')).toBe('Top')
|
||||
})
|
||||
})
|
||||
|
||||
test('Can change canvas zoom speed setting', async ({ comfyPage }) => {
|
||||
const [defaultSpeed, maxSpeed] = [1.1, 2.5]
|
||||
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(
|
||||
defaultSpeed
|
||||
)
|
||||
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed)
|
||||
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed)
|
||||
await comfyPage.page.reload()
|
||||
await comfyPage.setup()
|
||||
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed)
|
||||
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', defaultSpeed)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -32,4 +32,16 @@ test.describe('Optional input', () => {
|
||||
// If the node's multiline text widget is visible, then it was loaded successfully
|
||||
expect(comfyPage.page.locator('.comfy-multiline-input')).toHaveCount(1)
|
||||
})
|
||||
test('Old workflow with converted input', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('old_workflow_converted_input')
|
||||
const node = await comfyPage.getNodeRefById('1')
|
||||
const inputs = await node.getProperty('inputs')
|
||||
const vaeInput = inputs.find((w) => w.name === 'vae')
|
||||
const convertedInput = inputs.find((w) => w.name === 'strength')
|
||||
|
||||
expect(vaeInput).toBeDefined()
|
||||
expect(convertedInput).toBeDefined()
|
||||
expect(vaeInput.link).toBeNull()
|
||||
expect(convertedInput.link).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
69
browser_tests/utils/backupUtils.ts
Normal file
69
browser_tests/utils/backupUtils.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs-extra'
|
||||
|
||||
type PathParts = readonly [string, ...string[]]
|
||||
|
||||
const getBackupPath = (originalPath: string): string => `${originalPath}.bak`
|
||||
|
||||
const resolvePathIfExists = (pathParts: PathParts): string | null => {
|
||||
const resolvedPath = path.resolve(...pathParts)
|
||||
if (!fs.pathExistsSync(resolvedPath)) {
|
||||
console.warn(`Path not found: ${resolvedPath}`)
|
||||
return null
|
||||
}
|
||||
return resolvedPath
|
||||
}
|
||||
|
||||
const createScaffoldingCopy = (srcDir: string, destDir: string) => {
|
||||
// Get all items (files and directories) in the source directory
|
||||
const items = fs.readdirSync(srcDir, { withFileTypes: true })
|
||||
|
||||
for (const item of items) {
|
||||
const srcPath = path.join(srcDir, item.name)
|
||||
const destPath = path.join(destDir, item.name)
|
||||
|
||||
if (item.isDirectory()) {
|
||||
// Create the corresponding directory in the destination
|
||||
fs.ensureDirSync(destPath)
|
||||
|
||||
// Recursively copy the directory structure
|
||||
createScaffoldingCopy(srcPath, destPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function backupPath(
|
||||
pathParts: PathParts,
|
||||
{ renameAndReplaceWithScaffolding = false } = {}
|
||||
) {
|
||||
const originalPath = resolvePathIfExists(pathParts)
|
||||
if (!originalPath) return
|
||||
|
||||
const backupPath = getBackupPath(originalPath)
|
||||
try {
|
||||
if (renameAndReplaceWithScaffolding) {
|
||||
// Rename the original path and create scaffolding in its place
|
||||
fs.moveSync(originalPath, backupPath)
|
||||
createScaffoldingCopy(backupPath, originalPath)
|
||||
} else {
|
||||
// Create a copy of the original path
|
||||
fs.copySync(originalPath, backupPath)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to backup ${originalPath} from ${backupPath}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
export function restorePath(pathParts: PathParts) {
|
||||
const originalPath = resolvePathIfExists(pathParts)
|
||||
if (!originalPath) return
|
||||
|
||||
const backupPath = getBackupPath(originalPath)
|
||||
if (!fs.pathExistsSync(backupPath)) return
|
||||
|
||||
try {
|
||||
fs.moveSync(backupPath, originalPath, { overwrite: true })
|
||||
} catch (error) {
|
||||
console.error(`Failed to restore ${originalPath} from ${backupPath}`, error)
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,6 @@
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
<script type="module" src="node_modules/reflect-metadata/Reflect.js"></script>
|
||||
<script type="module" src="src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
755
package-lock.json
generated
755
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.3.10",
|
||||
"version": "1.3.19",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "npm run typecheck && vite build",
|
||||
"deploy": "npm run build && node scripts/deploy.js",
|
||||
"zipdist": "node scripts/zipdist.js",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"typecheck": "tsc --noEmit && tsc-strict",
|
||||
"format": "prettier --write './**/*.{js,ts,tsx,vue}'",
|
||||
"test": "npm run build && jest",
|
||||
"test:generate:examples": "npx tsx tests-ui/extractExamples",
|
||||
@@ -30,6 +30,7 @@
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "^20.14.8",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vue/vue3-jest": "^29.2.6",
|
||||
"autoprefixer": "^10.4.19",
|
||||
@@ -55,6 +56,7 @@
|
||||
"tsx": "^4.15.6",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"typescript-strict-plugin": "^2.4.4",
|
||||
"unplugin-icons": "^0.19.3",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"vite": "^5.4.6",
|
||||
@@ -63,20 +65,17 @@
|
||||
"zip-dir": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.2.1",
|
||||
"@comfyorg/litegraph": "^0.8.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/litegraph": "^0.8.3",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"axios": "^1.7.4",
|
||||
"class-transformer": "^0.5.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"fuse.js": "^7.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"pinia": "^2.1.7",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.0.5",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"vue": "^3.4.31",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.4.3",
|
||||
|
||||
@@ -30,6 +30,10 @@ export default defineConfig({
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry'
|
||||
},
|
||||
/* Path to global setup file. Exported function runs once before all the tests */
|
||||
globalSetup: './browser_tests/globalSetup.ts',
|
||||
/* Path to global teardown file. Exported function runs once after all the tests */
|
||||
globalTeardown: './browser_tests/globalTeardown.ts',
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
|
||||
@@ -15,8 +15,15 @@ import { useWorkspaceStore } from '@/stores/workspaceStateStore'
|
||||
import BlockUI from 'primevue/blockui'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
|
||||
const isLoading = computed<boolean>(() => useWorkspaceStore().spinner)
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const isLoading = computed<boolean>(() => workspaceStore.spinner)
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
workspaceStore.shiftDown = e.shiftKey
|
||||
}
|
||||
useEventListener(window, 'keydown', handleKey)
|
||||
useEventListener(window, 'keyup', handleKey)
|
||||
|
||||
onMounted(() => {
|
||||
window['__COMFYUI_FRONTEND_VERSION__'] = config.app_version
|
||||
|
||||
@@ -83,6 +83,7 @@ const setInitialPosition = () => {
|
||||
if (storedPosition.value.x !== 0 || storedPosition.value.y !== 0) {
|
||||
x.value = storedPosition.value.x
|
||||
y.value = storedPosition.value.y
|
||||
captureLastDragState()
|
||||
return
|
||||
}
|
||||
if (panelRef.value) {
|
||||
@@ -97,6 +98,7 @@ const setInitialPosition = () => {
|
||||
|
||||
x.value = (screenWidth - menuWidth) / 2
|
||||
y.value = screenHeight - menuHeight - 10 // 10px margin from bottom
|
||||
captureLastDragState()
|
||||
}
|
||||
}
|
||||
onMounted(setInitialPosition)
|
||||
@@ -106,6 +108,31 @@ watch(visible, (newVisible) => {
|
||||
}
|
||||
})
|
||||
|
||||
const lastDragState = ref({
|
||||
x: x.value,
|
||||
y: y.value,
|
||||
windowWidth: window.innerWidth,
|
||||
windowHeight: window.innerHeight
|
||||
})
|
||||
const captureLastDragState = () => {
|
||||
lastDragState.value = {
|
||||
x: x.value,
|
||||
y: y.value,
|
||||
windowWidth: window.innerWidth,
|
||||
windowHeight: window.innerHeight
|
||||
}
|
||||
}
|
||||
watch(
|
||||
isDragging,
|
||||
(newIsDragging) => {
|
||||
if (!newIsDragging) {
|
||||
// Stop dragging
|
||||
captureLastDragState()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const adjustMenuPosition = () => {
|
||||
if (panelRef.value) {
|
||||
const screenWidth = window.innerWidth
|
||||
@@ -113,10 +140,34 @@ const adjustMenuPosition = () => {
|
||||
const menuWidth = panelRef.value.offsetWidth
|
||||
const menuHeight = panelRef.value.offsetHeight
|
||||
|
||||
// Adjust x position if menu is off-screen horizontally
|
||||
x.value = clamp(x.value, 0, screenWidth - menuWidth)
|
||||
// Calculate the distance from each edge
|
||||
const distanceRight =
|
||||
lastDragState.value.windowWidth - (lastDragState.value.x + menuWidth)
|
||||
const distanceBottom =
|
||||
lastDragState.value.windowHeight - (lastDragState.value.y + menuHeight)
|
||||
|
||||
// Adjust y position if menu is off-screen vertically
|
||||
// Determine if the menu is closer to right/bottom or left/top
|
||||
const anchorRight = distanceRight < lastDragState.value.x
|
||||
const anchorBottom = distanceBottom < lastDragState.value.y
|
||||
|
||||
// Calculate new position
|
||||
if (anchorRight) {
|
||||
x.value =
|
||||
screenWidth - (lastDragState.value.windowWidth - lastDragState.value.x)
|
||||
} else {
|
||||
x.value = lastDragState.value.x
|
||||
}
|
||||
|
||||
if (anchorBottom) {
|
||||
y.value =
|
||||
screenHeight -
|
||||
(lastDragState.value.windowHeight - lastDragState.value.y)
|
||||
} else {
|
||||
y.value = lastDragState.value.y
|
||||
}
|
||||
|
||||
// Ensure the menu stays within the screen bounds
|
||||
x.value = clamp(x.value, 0, screenWidth - menuWidth)
|
||||
y.value = clamp(y.value, 0, screenHeight - menuHeight)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,20 @@
|
||||
<SplitButton
|
||||
class="comfyui-queue-button"
|
||||
:label="activeQueueModeMenuItem.label"
|
||||
:icon="activeQueueModeMenuItem.icon"
|
||||
severity="primary"
|
||||
@click="queuePrompt"
|
||||
:model="queueModeMenuItems"
|
||||
data-testid="queue-button"
|
||||
v-tooltip.bottom="$t('menu.queueWorkflow')"
|
||||
v-tooltip.bottom="
|
||||
workspaceStore.shiftDown
|
||||
? $t('menu.queueWorkflowFront')
|
||||
: $t('menu.queueWorkflow')
|
||||
"
|
||||
>
|
||||
<template #icon>
|
||||
<i-lucide:list-start v-if="workspaceStore.shiftDown" />
|
||||
<i v-else :class="activeQueueModeMenuItem.icon" />
|
||||
</template>
|
||||
<template #item="{ item }">
|
||||
<Button
|
||||
:label="item.label"
|
||||
@@ -58,7 +65,9 @@ import type { MenuItem } from 'primevue/menuitem'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
|
||||
const { mode: queueMode } = storeToRefs(useQueueSettingsStore())
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
<slot name="after-label" :node="props.node"></slot>
|
||||
</span>
|
||||
<Badge
|
||||
v-if="!props.node.leaf"
|
||||
:value="props.node.badgeText ?? props.node.totalLeaves"
|
||||
v-if="showNodeBadgeText"
|
||||
:value="nodeBadgeText"
|
||||
severity="secondary"
|
||||
class="leaf-count-badge"
|
||||
/>
|
||||
@@ -34,12 +34,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, inject, Ref, computed } from 'vue'
|
||||
import { ref, inject, Ref, computed } from 'vue'
|
||||
import Badge from 'primevue/badge'
|
||||
import {
|
||||
dropTargetForElements,
|
||||
draggable
|
||||
} from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
|
||||
import type {
|
||||
TreeExplorerDragAndDropData,
|
||||
RenderedTreeExplorerNode,
|
||||
@@ -47,6 +43,7 @@ import type {
|
||||
} from '@/types/treeExplorerTypes'
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
import { useErrorHandling } from '@/hooks/errorHooks'
|
||||
import { usePragmaticDraggable, usePragmaticDroppable } from '@/hooks/dndHooks'
|
||||
|
||||
const props = defineProps<{
|
||||
node: RenderedTreeExplorerNode
|
||||
@@ -62,6 +59,17 @@ const emit = defineEmits<{
|
||||
(e: 'dragEnd', node: RenderedTreeExplorerNode): void
|
||||
}>()
|
||||
|
||||
const nodeBadgeText = computed<string>(() => {
|
||||
if (props.node.leaf) {
|
||||
return ''
|
||||
}
|
||||
if (props.node.badgeText !== undefined && props.node.badgeText !== null) {
|
||||
return props.node.badgeText
|
||||
}
|
||||
return props.node.totalLeaves.toString()
|
||||
})
|
||||
const showNodeBadgeText = computed<boolean>(() => nodeBadgeText.value !== '')
|
||||
|
||||
const labelEditable = computed<boolean>(() => !!props.node.handleRename)
|
||||
const renameEditingNode =
|
||||
inject<Ref<TreeExplorerNode | null>>('renameEditingNode')
|
||||
@@ -78,54 +86,44 @@ const handleRename = errorHandling.wrapWithErrorHandlingAsync(
|
||||
)
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
const canDrop = ref(false)
|
||||
const treeNodeElement = ref<HTMLElement | null>(null)
|
||||
let dropTargetCleanup = () => {}
|
||||
let draggableCleanup = () => {}
|
||||
onMounted(() => {
|
||||
treeNodeElement.value = container.value?.closest(
|
||||
'.p-tree-node-content'
|
||||
) as HTMLElement
|
||||
if (props.node.droppable) {
|
||||
dropTargetCleanup = dropTargetForElements({
|
||||
element: treeNodeElement.value,
|
||||
onDrop: async (event) => {
|
||||
const dndData = event.source.data as TreeExplorerDragAndDropData
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
await props.node.handleDrop?.(props.node, dndData)
|
||||
canDrop.value = false
|
||||
emit('itemDropped', props.node, dndData.data)
|
||||
}
|
||||
},
|
||||
onDragEnter: (event) => {
|
||||
const dndData = event.source.data as TreeExplorerDragAndDropData
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
canDrop.value = true
|
||||
}
|
||||
},
|
||||
onDragLeave: () => {
|
||||
canDrop.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (props.node.draggable) {
|
||||
draggableCleanup = draggable({
|
||||
element: treeNodeElement.value,
|
||||
getInitialData() {
|
||||
return {
|
||||
type: 'tree-explorer-node',
|
||||
data: props.node
|
||||
}
|
||||
},
|
||||
onDragStart: () => emit('dragStart', props.node),
|
||||
onDrop: () => emit('dragEnd', props.node)
|
||||
})
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
dropTargetCleanup()
|
||||
draggableCleanup()
|
||||
})
|
||||
const treeNodeElementGetter = () =>
|
||||
container.value?.closest('.p-tree-node-content') as HTMLElement
|
||||
|
||||
if (props.node.draggable) {
|
||||
usePragmaticDraggable(treeNodeElementGetter, {
|
||||
getInitialData: () => {
|
||||
return {
|
||||
type: 'tree-explorer-node',
|
||||
data: props.node
|
||||
}
|
||||
},
|
||||
onDragStart: () => emit('dragStart', props.node),
|
||||
onDrop: () => emit('dragEnd', props.node)
|
||||
})
|
||||
}
|
||||
|
||||
if (props.node.droppable) {
|
||||
usePragmaticDroppable(treeNodeElementGetter, {
|
||||
onDrop: async (event) => {
|
||||
const dndData = event.source.data as TreeExplorerDragAndDropData
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
await props.node.handleDrop?.(props.node, dndData)
|
||||
canDrop.value = false
|
||||
emit('itemDropped', props.node, dndData.data)
|
||||
}
|
||||
},
|
||||
onDragEnter: (event) => {
|
||||
const dndData = event.source.data as TreeExplorerDragAndDropData
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
canDrop.value = true
|
||||
}
|
||||
},
|
||||
onDragLeave: () => {
|
||||
canDrop.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, it, expect, beforeAll } from 'vitest'
|
||||
import EditableText from '../EditableText.vue'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
|
||||
|
||||
@@ -12,13 +12,15 @@
|
||||
@maximize="onMaximize"
|
||||
@unmaximize="onUnmaximize"
|
||||
:pt="{ header: 'pb-0' }"
|
||||
:aria-labelledby="headerId"
|
||||
>
|
||||
<template #header>
|
||||
<component
|
||||
v-if="dialogStore.headerComponent"
|
||||
:is="dialogStore.headerComponent"
|
||||
:id="headerId"
|
||||
/>
|
||||
<h3 v-else>{{ dialogStore.title || ' ' }}</h3>
|
||||
<h3 v-else :id="headerId">{{ dialogStore.title || ' ' }}</h3>
|
||||
</template>
|
||||
|
||||
<component :is="dialogStore.component" v-bind="contentProps" />
|
||||
@@ -48,4 +50,6 @@ const contentProps = computed(() => ({
|
||||
...dialogStore.props,
|
||||
maximized: maximized.value
|
||||
}))
|
||||
|
||||
const headerId = `dialog-${Math.random().toString(36).substr(2, 9)}`
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="settings-container">
|
||||
<div class="settings-sidebar">
|
||||
<ScrollPanel class="settings-sidebar flex-shrink-0 p-2 w-64">
|
||||
<SearchBox
|
||||
class="settings-search-box"
|
||||
class="settings-search-box w-full mb-2"
|
||||
v-model:modelValue="searchQuery"
|
||||
@search="handleSearch"
|
||||
:placeholder="$t('searchSettings') + '...'"
|
||||
@@ -13,11 +13,11 @@
|
||||
optionLabel="label"
|
||||
scrollHeight="100%"
|
||||
:disabled="inSearch"
|
||||
:pt="{ root: { class: 'border-none' } }"
|
||||
class="border-none w-full"
|
||||
/>
|
||||
</div>
|
||||
</ScrollPanel>
|
||||
<Divider layout="vertical" />
|
||||
<div class="settings-content">
|
||||
<ScrollPanel class="settings-content flex-grow">
|
||||
<Tabs :value="tabValue">
|
||||
<TabPanels class="settings-tab-panels">
|
||||
<TabPanel key="search-results" value="Search Results">
|
||||
@@ -55,24 +55,35 @@
|
||||
<AboutPanel />
|
||||
</TabPanel>
|
||||
<TabPanel key="keybinding" value="Keybinding">
|
||||
<KeybindingPanel />
|
||||
<Suspense>
|
||||
<KeybindingPanel />
|
||||
<template #fallback>
|
||||
<div>Loading keybinding panel...</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
</TabPanel>
|
||||
<TabPanel key="extension" value="Extension">
|
||||
<ExtensionPanel />
|
||||
<Suspense>
|
||||
<ExtensionPanel />
|
||||
<template #fallback>
|
||||
<div>Loading extension panel...</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</div>
|
||||
</ScrollPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { ref, computed, onMounted, watch, defineAsyncComponent } from 'vue'
|
||||
import Listbox from 'primevue/listbox'
|
||||
import Tabs from 'primevue/tabs'
|
||||
import TabPanels from 'primevue/tabpanels'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import Divider from 'primevue/divider'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import { SettingTreeNode, useSettingStore } from '@/stores/settingStore'
|
||||
import { SettingParams } from '@/types/settingTypes'
|
||||
import SettingGroup from './setting/SettingGroup.vue'
|
||||
@@ -80,8 +91,13 @@ import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import { flattenTree } from '@/utils/treeUtil'
|
||||
import AboutPanel from './setting/AboutPanel.vue'
|
||||
import KeybindingPanel from './setting/KeybindingPanel.vue'
|
||||
import ExtensionPanel from './setting/ExtensionPanel.vue'
|
||||
|
||||
const KeybindingPanel = defineAsyncComponent(
|
||||
() => import('./setting/KeybindingPanel.vue')
|
||||
)
|
||||
const ExtensionPanel = defineAsyncComponent(
|
||||
() => import('./setting/ExtensionPanel.vue')
|
||||
)
|
||||
|
||||
interface ISettingGroup {
|
||||
label: string
|
||||
@@ -193,44 +209,8 @@ const tabValue = computed(() =>
|
||||
display: flex;
|
||||
height: 70vh;
|
||||
width: 60vw;
|
||||
max-width: 1000px;
|
||||
max-width: 1024px;
|
||||
overflow: hidden;
|
||||
/* Prevents container from scrolling */
|
||||
}
|
||||
|
||||
.settings-sidebar {
|
||||
width: 250px;
|
||||
flex-shrink: 0;
|
||||
/* Prevents sidebar from shrinking */
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.settings-search-box {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
/* Allows vertical scrolling */
|
||||
}
|
||||
|
||||
/* Ensure the Listbox takes full width of the sidebar */
|
||||
.settings-sidebar :deep(.p-listbox) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Optional: Style scrollbars for webkit browsers */
|
||||
.settings-sidebar::-webkit-scrollbar,
|
||||
.settings-content::-webkit-scrollbar {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.settings-sidebar::-webkit-scrollbar-thumb,
|
||||
.settings-content::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<canvas ref="canvasRef" id="graph-canvas" tabindex="1" />
|
||||
</teleport>
|
||||
<NodeSearchboxPopover />
|
||||
<NodeTooltip />
|
||||
<NodeTooltip v-if="tooltipEnabled" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -22,15 +22,10 @@ import SideToolbar from '@/components/sidebar/SideToolbar.vue'
|
||||
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
|
||||
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
|
||||
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
|
||||
import { ref, computed, onUnmounted, onMounted, watchEffect } from 'vue'
|
||||
import { ref, computed, onMounted, watchEffect } from 'vue'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
|
||||
import {
|
||||
ComfyNodeDefImpl,
|
||||
useNodeDefStore,
|
||||
useNodeFrequencyStore
|
||||
} from '@/stores/nodeDefStore'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
|
||||
import {
|
||||
LiteGraph,
|
||||
@@ -44,7 +39,6 @@ import {
|
||||
LGraphBadge
|
||||
} from '@comfyorg/litegraph'
|
||||
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { ComfyModelDef } from '@/stores/modelStore'
|
||||
import {
|
||||
@@ -52,7 +46,7 @@ import {
|
||||
useModelToNodeStore
|
||||
} from '@/stores/modelToNodeStore'
|
||||
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
|
||||
import { useKeybindingStore } from '@/stores/keybindingStore'
|
||||
import { usePragmaticDroppable } from '@/hooks/dndHooks'
|
||||
|
||||
const emit = defineEmits(['ready'])
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
@@ -67,6 +61,7 @@ const betaMenuEnabled = computed(
|
||||
const canvasMenuEnabled = computed(() =>
|
||||
settingStore.get('Comfy.Graph.CanvasMenu')
|
||||
)
|
||||
const tooltipEnabled = computed(() => settingStore.get('Comfy.EnableTooltips'))
|
||||
|
||||
watchEffect(() => {
|
||||
const canvasInfoEnabled = settingStore.get('Comfy.Graph.CanvasInfo')
|
||||
@@ -107,12 +102,12 @@ watchEffect(() => {
|
||||
watchEffect(() => {
|
||||
if (!canvasStore.canvas) return
|
||||
|
||||
if (canvasStore.draggingCanvas) {
|
||||
if (canvasStore.canvas.dragging_canvas) {
|
||||
canvasStore.canvas.canvas.style.cursor = 'grabbing'
|
||||
return
|
||||
}
|
||||
|
||||
if (canvasStore.readOnly) {
|
||||
if (canvasStore.canvas.read_only) {
|
||||
canvasStore.canvas.canvas.style.cursor = 'grab'
|
||||
return
|
||||
}
|
||||
@@ -120,7 +115,60 @@ watchEffect(() => {
|
||||
canvasStore.canvas.canvas.style.cursor = 'default'
|
||||
})
|
||||
|
||||
let dropTargetCleanup = () => {}
|
||||
usePragmaticDroppable(() => canvasRef.value, {
|
||||
onDrop: (event) => {
|
||||
const loc = event.location.current.input
|
||||
const dndData = event.source.data
|
||||
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
const node = dndData.data as RenderedTreeExplorerNode
|
||||
if (node.data instanceof ComfyNodeDefImpl) {
|
||||
const nodeDef = node.data
|
||||
// Add an offset on x to make sure after adding the node, the cursor
|
||||
// is on the node (top left corner)
|
||||
const pos = comfyApp.clientPosToCanvasPos([
|
||||
loc.clientX - 20,
|
||||
loc.clientY
|
||||
])
|
||||
comfyApp.addNodeOnGraph(nodeDef, { pos })
|
||||
} else if (node.data instanceof ComfyModelDef) {
|
||||
const model = node.data
|
||||
const pos = comfyApp.clientPosToCanvasPos([loc.clientX, loc.clientY])
|
||||
const nodeAtPos = comfyApp.graph.getNodeOnPos(pos[0], pos[1])
|
||||
let targetProvider: ModelNodeProvider | null = null
|
||||
let targetGraphNode: LGraphNode | null = null
|
||||
if (nodeAtPos) {
|
||||
const providers = modelToNodeStore.getAllNodeProviders(
|
||||
model.directory
|
||||
)
|
||||
for (const provider of providers) {
|
||||
if (provider.nodeDef.name === nodeAtPos.comfyClass) {
|
||||
targetGraphNode = nodeAtPos
|
||||
targetProvider = provider
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!targetGraphNode) {
|
||||
const provider = modelToNodeStore.getNodeProvider(model.directory)
|
||||
if (provider) {
|
||||
targetGraphNode = comfyApp.addNodeOnGraph(provider.nodeDef, {
|
||||
pos
|
||||
})
|
||||
targetProvider = provider
|
||||
}
|
||||
}
|
||||
if (targetGraphNode) {
|
||||
const widget = targetGraphNode.widgets.find(
|
||||
(widget) => widget.name === targetProvider.key
|
||||
)
|
||||
if (widget) {
|
||||
widget.value = model.file_name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
// Backward compatible
|
||||
@@ -145,78 +193,6 @@ onMounted(async () => {
|
||||
window['app'] = comfyApp
|
||||
window['graph'] = comfyApp.graph
|
||||
|
||||
dropTargetCleanup = dropTargetForElements({
|
||||
element: canvasRef.value,
|
||||
onDrop: (event) => {
|
||||
const loc = event.location.current.input
|
||||
const dndData = event.source.data
|
||||
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
const node = dndData.data as RenderedTreeExplorerNode
|
||||
if (node.data instanceof ComfyNodeDefImpl) {
|
||||
const nodeDef = node.data
|
||||
// Add an offset on x to make sure after adding the node, the cursor
|
||||
// is on the node (top left corner)
|
||||
const pos = comfyApp.clientPosToCanvasPos([
|
||||
loc.clientX - 20,
|
||||
loc.clientY
|
||||
])
|
||||
comfyApp.addNodeOnGraph(nodeDef, { pos })
|
||||
} else if (node.data instanceof ComfyModelDef) {
|
||||
const model = node.data
|
||||
const pos = comfyApp.clientPosToCanvasPos([loc.clientX, loc.clientY])
|
||||
const nodeAtPos = comfyApp.graph.getNodeOnPos(pos[0], pos[1])
|
||||
let targetProvider: ModelNodeProvider | null = null
|
||||
let targetGraphNode: LGraphNode | null = null
|
||||
if (nodeAtPos) {
|
||||
const providers = modelToNodeStore.getAllNodeProviders(
|
||||
model.directory
|
||||
)
|
||||
for (const provider of providers) {
|
||||
if (provider.nodeDef.name === nodeAtPos.comfyClass) {
|
||||
targetGraphNode = nodeAtPos
|
||||
targetProvider = provider
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!targetGraphNode) {
|
||||
const provider = modelToNodeStore.getNodeProvider(model.directory)
|
||||
if (provider) {
|
||||
targetGraphNode = comfyApp.addNodeOnGraph(provider.nodeDef, {
|
||||
pos
|
||||
})
|
||||
targetProvider = provider
|
||||
}
|
||||
}
|
||||
if (targetGraphNode) {
|
||||
const widget = targetGraphNode.widgets.find(
|
||||
(widget) => widget.name === targetProvider.key
|
||||
)
|
||||
if (widget) {
|
||||
widget.value = model.file_name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Load keybindings. This must be done after comfyApp loads settings.
|
||||
useKeybindingStore().loadUserKeybindings()
|
||||
|
||||
// Migrate legacy bookmarks
|
||||
useNodeBookmarkStore().migrateLegacyBookmarks()
|
||||
|
||||
// Explicitly initialize nodeSearchService to avoid indexing delay when
|
||||
// node search is triggered
|
||||
useNodeDefStore().nodeSearchService.endsWithFilterStartSequence('')
|
||||
|
||||
// Non-blocking load of node frequencies
|
||||
useNodeFrequencyStore().loadNodeFrequencies()
|
||||
emit('ready')
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
dropTargetCleanup()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -26,13 +26,16 @@
|
||||
severity="secondary"
|
||||
v-tooltip.left="
|
||||
t(
|
||||
'graphCanvasMenu.' + (canvasStore.readOnly ? 'panMode' : 'selectMode')
|
||||
'graphCanvasMenu.' +
|
||||
(canvasStore.canvas?.read_only ? 'panMode' : 'selectMode')
|
||||
) + ' (Space)'
|
||||
"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.ToggleLock')"
|
||||
>
|
||||
<template #icon>
|
||||
<i-material-symbols:pan-tool-outline v-if="canvasStore.readOnly" />
|
||||
<i-material-symbols:pan-tool-outline
|
||||
v-if="canvasStore.canvas?.read_only"
|
||||
/>
|
||||
<i-simple-line-icons:cursor v-else />
|
||||
</template>
|
||||
</Button>
|
||||
|
||||
@@ -10,15 +10,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref, onBeforeUnmount, watch } from 'vue'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
|
||||
let idleTimeout: number
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const settingStore = useSettingStore()
|
||||
const tooltipRef = ref<HTMLDivElement>()
|
||||
const tooltipText = ref('')
|
||||
const left = ref<string>()
|
||||
@@ -129,24 +128,8 @@ const onMouseMove = (e: MouseEvent) => {
|
||||
idleTimeout = window.setTimeout(onIdle, 500)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => settingStore.get('Comfy.EnableTooltips'),
|
||||
(enabled) => {
|
||||
if (enabled) {
|
||||
window.addEventListener('mousemove', onMouseMove)
|
||||
window.addEventListener('click', hideTooltip)
|
||||
} else {
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
window.removeEventListener('click', hideTooltip)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
window.removeEventListener('click', hideTooltip)
|
||||
})
|
||||
useEventListener(window, 'mousemove', onMouseMove)
|
||||
useEventListener(window, 'click', hideTooltip)
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
:key="tab.id"
|
||||
:icon="tab.icon"
|
||||
:iconBadge="tab.iconBadge"
|
||||
:tooltip="tab.tooltip"
|
||||
:tooltip="tab.tooltip + getTabTooltipSuffix(tab)"
|
||||
:selected="tab.id === selectedTab?.id"
|
||||
:class="tab.id + '-tab-button'"
|
||||
@click="onTabClick(tab)"
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
CustomSidebarTabExtension,
|
||||
SidebarTabExtension
|
||||
} from '@/types/extensionTypes'
|
||||
import { useKeybindingStore } from '@/stores/keybindingStore'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const settingStore = useSettingStore()
|
||||
@@ -74,6 +75,14 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const keybindingStore = useKeybindingStore()
|
||||
const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
|
||||
const keybinding = keybindingStore.getKeybindingByCommandId(
|
||||
`Workspace.ToggleSidebarTab.${tab.id}`
|
||||
)
|
||||
return keybinding ? ` (${keybinding.combo.toString()})` : ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -8,21 +8,19 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import SidebarIcon from './SidebarIcon.vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const previousDarkTheme = ref('dark')
|
||||
const currentTheme = computed(() => useSettingStore().get('Comfy.ColorPalette'))
|
||||
const isDarkMode = computed(() => currentTheme.value !== 'light')
|
||||
const icon = computed(() => (isDarkMode.value ? 'pi pi-moon' : 'pi pi-sun'))
|
||||
const settingStore = useSettingStore()
|
||||
const currentTheme = computed(() => settingStore.get('Comfy.ColorPalette'))
|
||||
const icon = computed(() =>
|
||||
currentTheme.value !== 'light' ? 'pi pi-moon' : 'pi pi-sun'
|
||||
)
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
const toggleTheme = () => {
|
||||
if (isDarkMode.value) {
|
||||
previousDarkTheme.value = currentTheme.value
|
||||
useSettingStore().set('Comfy.ColorPalette', 'light')
|
||||
} else {
|
||||
useSettingStore().set('Comfy.ColorPalette', previousDarkTheme.value)
|
||||
}
|
||||
commandStore.execute('Comfy.ToggleTheme')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
<template>
|
||||
<SidebarTabTemplate :title="$t('sideToolbar.modelLibrary')">
|
||||
<template #tool-buttons> </template>
|
||||
<SidebarTabTemplate
|
||||
:title="$t('sideToolbar.modelLibrary')"
|
||||
class="bg-[var(--p-tree-background)]"
|
||||
>
|
||||
<template #header>
|
||||
<SearchBox
|
||||
class="model-lib-search-box p-4"
|
||||
v-model:modelValue="searchQuery"
|
||||
:placeholder="$t('searchModels') + '...'"
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex-shrink-0">
|
||||
<SearchBox
|
||||
class="model-lib-search-box mx-4 mt-4"
|
||||
v-model:modelValue="searchQuery"
|
||||
:placeholder="$t('searchModels') + '...'"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-grow overflow-y-auto">
|
||||
<TreeExplorer
|
||||
class="model-lib-tree-explorer mt-1"
|
||||
:roots="renderedRoot.children"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
@nodeClick="handleNodeClick"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
<ModelTreeLeaf :node="node" />
|
||||
</template>
|
||||
</TreeExplorer>
|
||||
</div>
|
||||
</div>
|
||||
<TreeExplorer
|
||||
class="model-lib-tree-explorer py-0"
|
||||
:roots="renderedRoot.children"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
@nodeClick="handleNodeClick"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
<ModelTreeLeaf :node="node" />
|
||||
</template>
|
||||
</TreeExplorer>
|
||||
</template>
|
||||
</SidebarTabTemplate>
|
||||
<div id="model-library-model-preview-container" />
|
||||
@@ -70,7 +68,8 @@ const root: ComputedRef<TreeNode> = computed(() => {
|
||||
if (Object.values(models.models).length) {
|
||||
modelList.push(...Object.values(models.models))
|
||||
} else {
|
||||
const fakeModel = new ComfyModelDef('(No Content)', folder)
|
||||
// ModelDef with key 'folder/a/b/c/' is treated as empty folder
|
||||
const fakeModel = new ComfyModelDef('', folder)
|
||||
fakeModel.is_fake_object = true
|
||||
modelList.push(fakeModel)
|
||||
}
|
||||
@@ -83,15 +82,12 @@ const root: ComputedRef<TreeNode> = computed(() => {
|
||||
if (searchQuery.value) {
|
||||
const search = searchQuery.value.toLocaleLowerCase()
|
||||
modelList = modelList.filter((model: ComfyModelDef) => {
|
||||
return model.file_name.toLocaleLowerCase().includes(search)
|
||||
return model.searchable.includes(search)
|
||||
})
|
||||
}
|
||||
const tree: TreeNode = buildTree(modelList, (model: ComfyModelDef) => {
|
||||
return [
|
||||
model.directory,
|
||||
...model.file_name.replaceAll('\\', '/').split('/')
|
||||
]
|
||||
})
|
||||
const tree: TreeNode = buildTree(modelList, (model: ComfyModelDef) =>
|
||||
model.key.split('/')
|
||||
)
|
||||
return tree
|
||||
})
|
||||
|
||||
@@ -102,18 +98,7 @@ const renderedRoot = computed<TreeExplorerNode<ComfyModelDef>>(() => {
|
||||
const model: ComfyModelDef | null =
|
||||
node.leaf && node.data ? node.data : null
|
||||
if (model?.is_fake_object) {
|
||||
if (model.file_name === '(No Content)') {
|
||||
return {
|
||||
key: node.key,
|
||||
label: t('noContent'),
|
||||
leaf: true,
|
||||
data: node.data,
|
||||
getIcon: (node: TreeExplorerNode<ComfyModelDef>) => {
|
||||
return 'pi pi-file'
|
||||
},
|
||||
children: []
|
||||
}
|
||||
} else {
|
||||
if (model.file_name === 'Loading') {
|
||||
return {
|
||||
key: node.key,
|
||||
label: t('loading') + '...',
|
||||
@@ -151,10 +136,8 @@ const renderedRoot = computed<TreeExplorerNode<ComfyModelDef>>(() => {
|
||||
if (node.children?.length === 1) {
|
||||
const onlyChild = node.children[0]
|
||||
if (onlyChild.data?.is_fake_object) {
|
||||
if (onlyChild.data.file_name === '(No Content)') {
|
||||
return '0'
|
||||
} else if (onlyChild.data.file_name === 'Loading') {
|
||||
return '?'
|
||||
if (onlyChild.data.file_name === 'Loading') {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,15 +197,9 @@ watch(
|
||||
)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.pi-fake-spacer {
|
||||
<style scoped>
|
||||
:deep(.pi-fake-spacer) {
|
||||
height: 1px;
|
||||
width: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
:deep(.comfy-vue-side-bar-body) {
|
||||
background: var(--p-tree-background);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<SidebarTabTemplate :title="$t('sideToolbar.nodeLibrary')">
|
||||
<SidebarTabTemplate
|
||||
:title="$t('sideToolbar.nodeLibrary')"
|
||||
class="bg-[var(--p-tree-background)]"
|
||||
>
|
||||
<template #tool-buttons>
|
||||
<Button
|
||||
class="new-folder-button"
|
||||
@@ -18,44 +21,41 @@
|
||||
v-tooltip="$t('sideToolbar.nodeLibraryTab.sortOrder')"
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex-shrink-0">
|
||||
<SearchBox
|
||||
class="node-lib-search-box mx-4 mt-4"
|
||||
v-model:modelValue="searchQuery"
|
||||
@search="handleSearch"
|
||||
@show-filter="($event) => searchFilter.toggle($event)"
|
||||
@remove-filter="onRemoveFilter"
|
||||
:placeholder="$t('searchNodes') + '...'"
|
||||
filter-icon="pi pi-filter"
|
||||
:filters
|
||||
/>
|
||||
<template #header>
|
||||
<SearchBox
|
||||
class="node-lib-search-box p-4"
|
||||
v-model:modelValue="searchQuery"
|
||||
@search="handleSearch"
|
||||
@show-filter="($event) => searchFilter.toggle($event)"
|
||||
@remove-filter="onRemoveFilter"
|
||||
:placeholder="$t('searchNodes') + '...'"
|
||||
filter-icon="pi pi-filter"
|
||||
:filters
|
||||
/>
|
||||
|
||||
<Popover ref="searchFilter" class="node-lib-filter-popup">
|
||||
<NodeSearchFilter @addFilter="onAddFilter" />
|
||||
</Popover>
|
||||
</div>
|
||||
<div class="flex-grow overflow-y-auto">
|
||||
<NodeBookmarkTreeExplorer
|
||||
ref="nodeBookmarkTreeExplorerRef"
|
||||
:filtered-node-defs="filteredNodeDefs"
|
||||
/>
|
||||
<Divider
|
||||
v-if="nodeBookmarkStore.bookmarks.length > 0"
|
||||
type="dashed"
|
||||
/>
|
||||
<TreeExplorer
|
||||
class="node-lib-tree-explorer mt-1"
|
||||
:roots="renderedRoot.children"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
<NodeTreeLeaf :node="node" />
|
||||
</template>
|
||||
</TreeExplorer>
|
||||
</div>
|
||||
</div>
|
||||
<Popover ref="searchFilter" class="ml-[-13px]">
|
||||
<NodeSearchFilter @addFilter="onAddFilter" />
|
||||
</Popover>
|
||||
</template>
|
||||
<template #body>
|
||||
<NodeBookmarkTreeExplorer
|
||||
ref="nodeBookmarkTreeExplorerRef"
|
||||
:filtered-node-defs="filteredNodeDefs"
|
||||
/>
|
||||
<Divider
|
||||
v-show="nodeBookmarkStore.bookmarks.length > 0"
|
||||
type="dashed"
|
||||
class="m-2"
|
||||
/>
|
||||
<TreeExplorer
|
||||
class="node-lib-tree-explorer py-0"
|
||||
:roots="renderedRoot.children"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
<NodeTreeLeaf :node="node" />
|
||||
</template>
|
||||
</TreeExplorer>
|
||||
</template>
|
||||
</SidebarTabTemplate>
|
||||
<div id="node-library-node-preview-container" />
|
||||
@@ -193,23 +193,3 @@ const onRemoveFilter = (filterAndValue) => {
|
||||
handleSearch(searchQuery.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.node-lib-filter-popup {
|
||||
margin-left: -13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
:deep(.comfy-vue-side-bar-body) {
|
||||
background: var(--p-tree-background);
|
||||
}
|
||||
|
||||
:deep(.node-lib-bookmark-tree-explorer) {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
:deep(.p-divider) {
|
||||
margin: var(--comfy-tree-explorer-item-padding) 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -249,7 +249,8 @@ const menuItems = computed<MenuItem[]>(() => [
|
||||
{
|
||||
label: t('loadWorkflow'),
|
||||
icon: 'pi pi-file-export',
|
||||
command: () => menuTargetTask.value?.loadWorkflow()
|
||||
command: () => menuTargetTask.value?.loadWorkflow(app),
|
||||
disabled: !menuTargetTask.value?.workflow
|
||||
},
|
||||
{
|
||||
label: t('goToNode'),
|
||||
@@ -327,6 +328,14 @@ watch(
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.scroll-container::-webkit-scrollbar {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.scroll-container::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.queue-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
|
||||
@@ -1,66 +1,36 @@
|
||||
<template>
|
||||
<div class="comfy-vue-side-bar-container">
|
||||
<Toolbar class="comfy-vue-side-bar-header">
|
||||
<template #start>
|
||||
<span class="comfy-vue-side-bar-header-span">{{
|
||||
props.title.toUpperCase()
|
||||
}}</span>
|
||||
</template>
|
||||
<template #end>
|
||||
<slot name="tool-buttons"></slot>
|
||||
</template>
|
||||
</Toolbar>
|
||||
<div class="comfy-vue-side-bar-body">
|
||||
<slot name="body"></slot>
|
||||
<div
|
||||
class="comfy-vue-side-bar-container flex flex-col h-full"
|
||||
:class="props.class"
|
||||
>
|
||||
<div class="comfy-vue-side-bar-header">
|
||||
<Toolbar
|
||||
class="flex-shrink-0 border-x-0 border-t-0 rounded-none px-2 py-1 min-h-10"
|
||||
>
|
||||
<template #start>
|
||||
<span class="text-sm">{{ props.title.toUpperCase() }}</span>
|
||||
</template>
|
||||
<template #end>
|
||||
<slot name="tool-buttons"></slot>
|
||||
</template>
|
||||
</Toolbar>
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<!-- h-0 to force scrollpanel to flex-grow -->
|
||||
<ScrollPanel class="comfy-vue-side-bar-body flex-grow h-0">
|
||||
<div class="h-full">
|
||||
<slot name="body"></slot>
|
||||
</div>
|
||||
</ScrollPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Toolbar from 'primevue/toolbar'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
class?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.comfy-vue-side-bar-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.comfy-vue-side-bar-header {
|
||||
flex-shrink: 0;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
border-radius: 0;
|
||||
padding: 0.25rem 1rem;
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
|
||||
.comfy-vue-side-bar-header-span {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.comfy-vue-side-bar-body {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: transparent transparent;
|
||||
}
|
||||
|
||||
.comfy-vue-side-bar-body::-webkit-scrollbar {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.comfy-vue-side-bar-body::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<SidebarTabTemplate :title="$t('sideToolbar.workflows')">
|
||||
<SidebarTabTemplate
|
||||
:title="$t('sideToolbar.workflows')"
|
||||
class="bg-[var(--p-tree-background)]"
|
||||
>
|
||||
<template #tool-buttons>
|
||||
<Button
|
||||
class="browse-templates-button"
|
||||
@@ -23,13 +26,15 @@
|
||||
text
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<template #header>
|
||||
<SearchBox
|
||||
class="workflows-search-box mx-4 my-4"
|
||||
class="workflows-search-box p-4"
|
||||
v-model:modelValue="searchQuery"
|
||||
@search="handleSearch"
|
||||
:placeholder="$t('searchWorkflows') + '...'"
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="comfyui-workflows-panel" v-if="!isSearching">
|
||||
<div
|
||||
class="comfyui-workflows-open"
|
||||
@@ -214,9 +219,3 @@ const selectionKeys = computed(() => ({
|
||||
[`root/${workflowStore.activeWorkflow?.name}.json`]: true
|
||||
}))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.comfy-vue-side-bar-body) {
|
||||
background: var(--p-tree-background);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<TreeExplorer
|
||||
class="node-lib-bookmark-tree-explorer"
|
||||
class="node-lib-bookmark-tree-explorer py-0"
|
||||
ref="treeExplorerRef"
|
||||
:roots="renderedBookmarkedRoot.children"
|
||||
:expandedKeys="expandedKeys"
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
:src="item.url"
|
||||
:contain="false"
|
||||
class="galleria-image"
|
||||
v-if="item.isImage"
|
||||
/>
|
||||
<ResultVideo v-else-if="item.isVideo" :result="item" />
|
||||
</template>
|
||||
</Galleria>
|
||||
</template>
|
||||
@@ -35,6 +37,7 @@ import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import Galleria from 'primevue/galleria'
|
||||
import { ResultItemImpl } from '@/stores/queueStore'
|
||||
import ComfyImage from '@/components/common/ComfyImage.vue'
|
||||
import ResultVideo from './ResultVideo.vue'
|
||||
|
||||
const galleryVisible = ref(false)
|
||||
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
<template>
|
||||
<div class="result-container" ref="resultContainer">
|
||||
<template
|
||||
v-if="result.mediaType === 'images' || result.mediaType === 'gifs'"
|
||||
>
|
||||
<ComfyImage
|
||||
:src="result.url"
|
||||
class="task-output-image"
|
||||
:contain="imageFit === 'contain'"
|
||||
/>
|
||||
<div class="image-preview-mask">
|
||||
<Button
|
||||
icon="pi pi-eye"
|
||||
severity="secondary"
|
||||
@click="emit('preview', result)"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<!-- TODO: handle more media types -->
|
||||
<ComfyImage
|
||||
v-if="result.isImage"
|
||||
:src="result.url"
|
||||
class="task-output-image"
|
||||
:contain="imageFit === 'contain'"
|
||||
/>
|
||||
<ResultVideo v-else-if="result.isVideo" :result="result" />
|
||||
<div v-else class="task-result-preview">
|
||||
<i class="pi pi-file"></i>
|
||||
<span>{{ result.mediaType }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="result.supportsPreview" class="preview-mask">
|
||||
<Button
|
||||
icon="pi pi-eye"
|
||||
severity="secondary"
|
||||
@click="emit('preview', result)"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -31,6 +29,7 @@ import ComfyImage from '@/components/common/ComfyImage.vue'
|
||||
import Button from 'primevue/button'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import ResultVideo from './ResultVideo.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
result: ResultItemImpl
|
||||
@@ -67,7 +66,7 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.image-preview-mask {
|
||||
.preview-mask {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
@@ -80,7 +79,7 @@ onMounted(() => {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.result-container:hover .image-preview-mask {
|
||||
.result-container:hover .preview-mask {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
30
src/components/sidebar/tabs/queue/ResultVideo.vue
Normal file
30
src/components/sidebar/tabs/queue/ResultVideo.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<video controls width="100%" height="100%">
|
||||
<source :src="url" :type="htmlVideoType" />
|
||||
{{ $t('videoFailedToLoad') }}
|
||||
</video>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ResultItemImpl } from '@/stores/queueStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
result: ResultItemImpl
|
||||
}>()
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const vhsAdvancedPreviews = computed(() =>
|
||||
settingStore.get('VHS.AdvancedPreviews')
|
||||
)
|
||||
|
||||
const url = computed(() =>
|
||||
vhsAdvancedPreviews.value
|
||||
? props.result.vhsAdvancedPreviewUrl
|
||||
: props.result.url
|
||||
)
|
||||
const htmlVideoType = computed(() =>
|
||||
vhsAdvancedPreviews.value ? 'video/webm' : props.result.htmlVideoType
|
||||
)
|
||||
</script>
|
||||
@@ -83,11 +83,12 @@ const coverResult = flatOutputs.length
|
||||
? props.task.previewOutput || flatOutputs[0]
|
||||
: null
|
||||
// Using `==` instead of `===` because NodeId can be a string or a number
|
||||
const node: ComfyNode | null = flatOutputs.length
|
||||
? props.task.workflow.nodes.find(
|
||||
(n: ComfyNode) => n.id == coverResult.nodeId
|
||||
) ?? null
|
||||
: null
|
||||
const node: ComfyNode | null =
|
||||
flatOutputs.length && props.task.workflow
|
||||
? props.task.workflow.nodes.find(
|
||||
(n: ComfyNode) => n.id == coverResult.nodeId
|
||||
) ?? null
|
||||
: null
|
||||
const progressPreviewBlobUrl = ref('')
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// @ts-strict-ignore
|
||||
// Disabled because of https://github.com/Comfy-Org/ComfyUI_frontend/issues/1184
|
||||
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { expect, describe, it } from 'vitest'
|
||||
import ResultGallery from '../ResultGallery.vue'
|
||||
@@ -12,15 +15,12 @@ describe('ResultGallery', () => {
|
||||
let mockResultItem: ResultItemImpl
|
||||
|
||||
beforeEach(() => {
|
||||
mockResultItem = {
|
||||
mockResultItem = new ResultItemImpl({
|
||||
filename: 'test.jpg',
|
||||
type: 'images',
|
||||
nodeId: 'test',
|
||||
mediaType: 'images',
|
||||
url: 'https://picsum.photos/200/300',
|
||||
urlWithTimestamp: 'https://picsum.photos/200/300?t=123456',
|
||||
supportsPreview: true
|
||||
}
|
||||
nodeId: 1,
|
||||
mediaType: 'images'
|
||||
})
|
||||
})
|
||||
|
||||
const mountResultGallery = (props: ResultGalleryProps, options = {}) => {
|
||||
@@ -9,7 +9,12 @@
|
||||
}"
|
||||
>
|
||||
<template #item="{ item, props }">
|
||||
<a class="p-menubar-item-link" v-bind="props.action">
|
||||
<a
|
||||
class="p-menubar-item-link"
|
||||
v-bind="props.action"
|
||||
:href="item.url"
|
||||
target="_blank"
|
||||
>
|
||||
<span v-if="item.icon" class="p-menubar-item-icon" :class="item.icon" />
|
||||
<span class="p-menubar-item-label">{{ item.label }}</span>
|
||||
<span
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { ComfyDialog, $el } from '../../scripts/ui'
|
||||
import { ComfyApp } from '../../scripts/app'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { app } from '../../scripts/app'
|
||||
import { $el } from '../../scripts/ui'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { LiteGraph, LGraphCanvas } from '@comfyorg/litegraph'
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
// Allows for simple dynamic prompt replacement
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
// Allows you to edit the attention weight by holding ctrl (or cmd) and using the up/down arrow keys
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { api } from '../../scripts/api'
|
||||
import { mergeIfValid } from './widgetInputs'
|
||||
@@ -7,6 +8,7 @@ import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { ComfyLink, ComfyNode, ComfyWorkflowJSON } from '@/types/comfyWorkflow'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { ComfyExtension } from '@/types/comfy'
|
||||
|
||||
type GroupNodeWorkflowData = {
|
||||
external: ComfyLink[]
|
||||
@@ -52,7 +54,7 @@ class GroupNodeBuilder {
|
||||
nodes: LGraphNode[]
|
||||
nodeData: any
|
||||
|
||||
constructor(nodes) {
|
||||
constructor(nodes: LGraphNode[]) {
|
||||
this.nodes = nodes
|
||||
}
|
||||
|
||||
@@ -995,9 +997,7 @@ export class GroupNodeHandler {
|
||||
},
|
||||
{
|
||||
content: 'Manage Group Node',
|
||||
callback: () => {
|
||||
new ManageGroupDialog(app).show(this.type)
|
||||
}
|
||||
callback: manageGroupNodes
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1367,11 +1367,11 @@ export class GroupNodeHandler {
|
||||
return (node.nodeData ?? node.constructor?.nodeData)?.[GROUP]
|
||||
}
|
||||
|
||||
static isGroupNode(node) {
|
||||
static isGroupNode(node: LGraphNode) {
|
||||
return !!node.constructor?.nodeData?.[GROUP]
|
||||
}
|
||||
|
||||
static async fromNodes(nodes) {
|
||||
static async fromNodes(nodes: LGraphNode[]) {
|
||||
// Process the nodes into the stored workflow group node data
|
||||
const builder = new GroupNodeBuilder(nodes)
|
||||
const res = builder.build()
|
||||
@@ -1404,9 +1404,7 @@ function addConvertToGroupOptions() {
|
||||
options.splice(index + 1, null, {
|
||||
content: `Convert to Group Node`,
|
||||
disabled,
|
||||
callback: async () => {
|
||||
return await GroupNodeHandler.fromNodes(selected)
|
||||
}
|
||||
callback: convertSelectedNodesToGroupNode
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1416,9 +1414,7 @@ function addConvertToGroupOptions() {
|
||||
options.splice(index + 1, null, {
|
||||
content: `Manage Group Nodes`,
|
||||
disabled,
|
||||
callback: () => {
|
||||
new ManageGroupDialog(app).show()
|
||||
}
|
||||
callback: manageGroupNodes
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1455,10 +1451,86 @@ const replaceLegacySeparators = (nodes: ComfyNode[]): void => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert selected nodes to a group node
|
||||
* @throws {Error} if no nodes are selected
|
||||
* @throws {Error} if a group node is already selected
|
||||
* @throws {Error} if a group node is selected
|
||||
*
|
||||
* The context menu item should not be available if any of the above conditions are met.
|
||||
* The error is automatically handled by the commandStore when the command is executed.
|
||||
*/
|
||||
async function convertSelectedNodesToGroupNode() {
|
||||
const nodes = Object.values(app.canvas.selected_nodes ?? {})
|
||||
if (nodes.length === 0) {
|
||||
throw new Error('No nodes selected')
|
||||
}
|
||||
if (nodes.length === 1) {
|
||||
throw new Error('Please select multiple nodes to convert to group node')
|
||||
}
|
||||
if (nodes.some((n) => GroupNodeHandler.isGroupNode(n))) {
|
||||
throw new Error('Selected nodes contain a group node')
|
||||
}
|
||||
return await GroupNodeHandler.fromNodes(nodes)
|
||||
}
|
||||
|
||||
function ungroupSelectedGroupNodes() {
|
||||
const nodes = Object.values(app.canvas.selected_nodes ?? {})
|
||||
for (const node of nodes) {
|
||||
if (GroupNodeHandler.isGroupNode(node)) {
|
||||
node['convertToNodes']?.()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function manageGroupNodes() {
|
||||
new ManageGroupDialog(app).show()
|
||||
}
|
||||
|
||||
const id = 'Comfy.GroupNode'
|
||||
let globalDefs
|
||||
const ext = {
|
||||
const ext: ComfyExtension = {
|
||||
name: id,
|
||||
commands: [
|
||||
{
|
||||
id: 'Comfy.GroupNode.ConvertSelectedNodesToGroupNode',
|
||||
label: 'Convert selected nodes to group node',
|
||||
icon: 'pi pi-sitemap',
|
||||
versionAdded: '1.3.17',
|
||||
function: convertSelectedNodesToGroupNode
|
||||
},
|
||||
{
|
||||
id: 'Comfy.GroupNode.UngroupSelectedGroupNodes',
|
||||
label: 'Ungroup selected group nodes',
|
||||
icon: 'pi pi-sitemap',
|
||||
versionAdded: '1.3.17',
|
||||
function: ungroupSelectedGroupNodes
|
||||
},
|
||||
{
|
||||
id: 'Comfy.GroupNode.ManageGroupNodes',
|
||||
label: 'Manage group nodes',
|
||||
icon: 'pi pi-cog',
|
||||
versionAdded: '1.3.17',
|
||||
function: manageGroupNodes
|
||||
}
|
||||
],
|
||||
keybindings: [
|
||||
{
|
||||
commandId: 'Comfy.GroupNode.ConvertSelectedNodesToGroupNode',
|
||||
combo: {
|
||||
alt: true,
|
||||
key: 'g'
|
||||
}
|
||||
},
|
||||
{
|
||||
commandId: 'Comfy.GroupNode.UngroupSelectedGroupNodes',
|
||||
combo: {
|
||||
alt: true,
|
||||
shift: true,
|
||||
key: 'G'
|
||||
}
|
||||
}
|
||||
],
|
||||
setup() {
|
||||
addConvertToGroupOptions()
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { $el, ComfyDialog } from '../../scripts/ui'
|
||||
import { DraggableList } from '../../scripts/ui/draggableList'
|
||||
import { GroupNodeConfig, GroupNodeHandler } from './groupNode'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { LGraphGroup } from '@comfyorg/litegraph'
|
||||
import { app } from '../../scripts/app'
|
||||
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ app.registerExtension({
|
||||
const keybindingStore = useKeybindingStore()
|
||||
const commandStore = useCommandStore()
|
||||
const keybinding = keybindingStore.getKeybinding(keyCombo)
|
||||
if (keybinding) {
|
||||
if (keybinding && keybinding.targetSelector !== '#graph-canvas') {
|
||||
await commandStore.execute(keybinding.commandId)
|
||||
event.preventDefault()
|
||||
return
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
const id = 'Comfy.LinkRenderMode'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { ComfyDialog, $el } from '../../scripts/ui'
|
||||
import { ComfyApp } from '../../scripts/app'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app, type ComfyApp } from '@/scripts/app'
|
||||
import type { ComfyExtension } from '@/types/comfy'
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { api } from '../../scripts/api'
|
||||
import { ComfyDialog, $el } from '../../scripts/ui'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { LiteGraph, LGraphCanvas } from '@comfyorg/litegraph'
|
||||
import { app } from '../../scripts/app'
|
||||
import { ComfyWidgets } from '../../scripts/widgets'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import type { IContextMenuValue } from '@comfyorg/litegraph'
|
||||
import { app } from '../../scripts/app'
|
||||
import { mergeIfValid, getWidgetConfig, setWidgetConfig } from './widgetInputs'
|
||||
@@ -159,7 +160,6 @@ app.registerExtension({
|
||||
for (const l of node.outputs[0].links || []) {
|
||||
const link = app.graph.links[l]
|
||||
if (link) {
|
||||
// @ts-expect-error Fix litegraph types
|
||||
link.color = color
|
||||
|
||||
if (app.configuringGraph) continue
|
||||
@@ -205,7 +205,6 @@ app.registerExtension({
|
||||
if (inputNode) {
|
||||
const link = app.graph.links[inputNode.inputs[0].link]
|
||||
if (link) {
|
||||
// @ts-expect-error Fix litegraph types
|
||||
link.color = color
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { applyTextReplacements } from '../../scripts/utils'
|
||||
// Use widget values and dates in output filenames
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { ComfyWidgets } from '../../scripts/widgets'
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// @ts-strict-ignore
|
||||
import type { SettingParams } from '@/types/settingTypes'
|
||||
import { app } from '../../scripts/app'
|
||||
import {
|
||||
LGraphCanvas,
|
||||
@@ -37,13 +39,25 @@ app.registerExtension({
|
||||
LiteGraph.CANVAS_GRID_SIZE = +value || 10
|
||||
}
|
||||
})
|
||||
// Keep the 'pysssss.SnapToGrid' setting id so we don't need to migrate setting values.
|
||||
// Using a new setting id can cause existing users to lose their existing settings.
|
||||
const alwaysSnapToGrid = app.ui.settings.addSetting({
|
||||
id: 'pysssss.SnapToGrid',
|
||||
category: ['Comfy', 'Graph', 'AlwaysSnapToGrid'],
|
||||
name: 'Always snap to grid',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
versionAdded: '1.3.13'
|
||||
} as SettingParams)
|
||||
|
||||
const shouldSnapToGrid = () => app.shiftDown || alwaysSnapToGrid.value
|
||||
|
||||
// After moving a node, if the shift key is down align it to grid
|
||||
const onNodeMoved = app.canvas.onNodeMoved
|
||||
app.canvas.onNodeMoved = function (node) {
|
||||
const r = onNodeMoved?.apply(this, arguments)
|
||||
|
||||
if (app.shiftDown) {
|
||||
if (shouldSnapToGrid()) {
|
||||
// Ensure all selected nodes are realigned
|
||||
for (const id in this.selected_nodes) {
|
||||
this.selected_nodes[id].alignToGrid()
|
||||
@@ -58,7 +72,7 @@ app.registerExtension({
|
||||
app.graph.onNodeAdded = function (node) {
|
||||
const onResize = node.onResize
|
||||
node.onResize = function () {
|
||||
if (app.shiftDown) {
|
||||
if (shouldSnapToGrid()) {
|
||||
roundVectorToGrid(node.size)
|
||||
}
|
||||
return onResize?.apply(this, arguments)
|
||||
@@ -70,7 +84,7 @@ app.registerExtension({
|
||||
const origDrawNode = LGraphCanvas.prototype.drawNode
|
||||
LGraphCanvas.prototype.drawNode = function (node, ctx) {
|
||||
if (
|
||||
app.shiftDown &&
|
||||
shouldSnapToGrid() &&
|
||||
this.node_dragged &&
|
||||
node.id in this.selected_nodes
|
||||
) {
|
||||
@@ -132,8 +146,8 @@ app.registerExtension({
|
||||
// to snap on a mouse-up which we can determine by checking if `app.canvas.last_mouse_dragging`
|
||||
// has been set to `false`. Essentially, this check here is the equivalent to calling an
|
||||
// `LGraphGroup.prototype.onNodeMoved` if it had existed.
|
||||
if (app.canvas.last_mouse_dragging === false && app.shiftDown) {
|
||||
// After moving a group (while app.shiftDown), snap all the child nodes and, finally,
|
||||
if (app.canvas.last_mouse_dragging === false && shouldSnapToGrid()) {
|
||||
// After moving a group (while shouldSnapToGrid()), snap all the child nodes and, finally,
|
||||
// align the group itself.
|
||||
this.recomputeInsideNodes()
|
||||
for (const node of this.nodes) {
|
||||
@@ -151,7 +165,7 @@ app.registerExtension({
|
||||
*/
|
||||
const drawGroups = LGraphCanvas.prototype.drawGroups
|
||||
LGraphCanvas.prototype.drawGroups = function (canvas, ctx) {
|
||||
if (this.selected_group && app.shiftDown) {
|
||||
if (this.selected_group && shouldSnapToGrid()) {
|
||||
if (this.selected_group_resizing) {
|
||||
roundVectorToGrid(this.selected_group.size)
|
||||
} else if (selectedAndMovingGroup) {
|
||||
@@ -176,7 +190,7 @@ app.registerExtension({
|
||||
const onGroupAdd = LGraphCanvas.onGroupAdd
|
||||
LGraphCanvas.onGroupAdd = function () {
|
||||
const v = onGroupAdd.apply(app.canvas, arguments)
|
||||
if (app.shiftDown) {
|
||||
if (shouldSnapToGrid()) {
|
||||
const lastGroup = app.graph.groups[app.graph.groups.length - 1]
|
||||
if (lastGroup) {
|
||||
roundVectorToGrid(lastGroup.pos)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { api } from '../../scripts/api'
|
||||
import type { IWidget } from '@comfyorg/litegraph'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { ComfyNodeDef } from '@/types/apiTypes'
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { api } from '../../scripts/api'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { ComfyWidgets, addValueControlWidgets } from '../../scripts/widgets'
|
||||
import { app } from '../../scripts/app'
|
||||
import { applyTextReplacements } from '../../scripts/utils'
|
||||
|
||||
59
src/hooks/dndHooks.ts
Normal file
59
src/hooks/dndHooks.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
import {
|
||||
dropTargetForElements,
|
||||
draggable
|
||||
} from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
|
||||
|
||||
export function usePragmaticDroppable(
|
||||
dropTargetElement: HTMLElement | (() => HTMLElement),
|
||||
options: Omit<Parameters<typeof dropTargetForElements>[0], 'element'>
|
||||
) {
|
||||
let cleanup = () => {}
|
||||
|
||||
onMounted(() => {
|
||||
const element =
|
||||
typeof dropTargetElement === 'function'
|
||||
? dropTargetElement()
|
||||
: dropTargetElement
|
||||
|
||||
if (!element) {
|
||||
return
|
||||
}
|
||||
|
||||
cleanup = dropTargetForElements({
|
||||
element,
|
||||
...options
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanup()
|
||||
})
|
||||
}
|
||||
|
||||
export function usePragmaticDraggable(
|
||||
draggableElement: HTMLElement | (() => HTMLElement),
|
||||
options: Omit<Parameters<typeof draggable>[0], 'element'>
|
||||
) {
|
||||
let cleanup = () => {}
|
||||
|
||||
onMounted(() => {
|
||||
const element =
|
||||
typeof draggableElement === 'function'
|
||||
? draggableElement()
|
||||
: draggableElement
|
||||
|
||||
if (!element) {
|
||||
return
|
||||
}
|
||||
|
||||
cleanup = draggable({
|
||||
element,
|
||||
...options
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanup()
|
||||
})
|
||||
}
|
||||
16
src/hooks/sidebarTabs/modelLibrarySidebarTab.ts
Normal file
16
src/hooks/sidebarTabs/modelLibrarySidebarTab.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { markRaw } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ModelLibrarySidebarTab from '@/components/sidebar/tabs/ModelLibrarySidebarTab.vue'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
export const useModelLibrarySidebarTab = (): SidebarTabExtension => {
|
||||
const { t } = useI18n()
|
||||
return {
|
||||
id: 'model-library',
|
||||
icon: 'pi pi-box',
|
||||
title: t('sideToolbar.modelLibrary'),
|
||||
tooltip: t('sideToolbar.modelLibrary'),
|
||||
component: markRaw(ModelLibrarySidebarTab),
|
||||
type: 'vue'
|
||||
}
|
||||
}
|
||||
16
src/hooks/sidebarTabs/nodeLibrarySidebarTab.ts
Normal file
16
src/hooks/sidebarTabs/nodeLibrarySidebarTab.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { markRaw } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import NodeLibrarySidebarTab from '@/components/sidebar/tabs/NodeLibrarySidebarTab.vue'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
export const useNodeLibrarySidebarTab = (): SidebarTabExtension => {
|
||||
const { t } = useI18n()
|
||||
return {
|
||||
id: 'node-library',
|
||||
icon: 'pi pi-book',
|
||||
title: t('sideToolbar.nodeLibrary'),
|
||||
tooltip: t('sideToolbar.nodeLibrary'),
|
||||
component: markRaw(NodeLibrarySidebarTab),
|
||||
type: 'vue'
|
||||
}
|
||||
}
|
||||
22
src/hooks/sidebarTabs/queueSidebarTab.ts
Normal file
22
src/hooks/sidebarTabs/queueSidebarTab.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useQueuePendingTaskCountStore } from '@/stores/queueStore'
|
||||
import { markRaw } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import QueueSidebarTab from '@/components/sidebar/tabs/QueueSidebarTab.vue'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
export const useQueueSidebarTab = (): SidebarTabExtension => {
|
||||
const { t } = useI18n()
|
||||
const queuePendingTaskCountStore = useQueuePendingTaskCountStore()
|
||||
return {
|
||||
id: 'queue',
|
||||
icon: 'pi pi-history',
|
||||
iconBadge: () => {
|
||||
const value = queuePendingTaskCountStore.count.toString()
|
||||
return value === '0' ? null : value
|
||||
},
|
||||
title: t('sideToolbar.queue'),
|
||||
tooltip: t('sideToolbar.queue'),
|
||||
component: markRaw(QueueSidebarTab),
|
||||
type: 'vue'
|
||||
}
|
||||
}
|
||||
30
src/hooks/sidebarTabs/workflowsSidebarTab.ts
Normal file
30
src/hooks/sidebarTabs/workflowsSidebarTab.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { markRaw } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import WorkflowsSidebarTab from '@/components/sidebar/tabs/WorkflowsSidebarTab.vue'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
export const useWorkflowsSidebarTab = (): SidebarTabExtension => {
|
||||
const { t } = useI18n()
|
||||
const settingStore = useSettingStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
return {
|
||||
id: 'workflows',
|
||||
icon: 'pi pi-folder-open',
|
||||
iconBadge: () => {
|
||||
if (
|
||||
settingStore.get('Comfy.Workflow.WorkflowTabsPosition') !== 'Sidebar'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const value = workflowStore.openWorkflows.length.toString()
|
||||
return value === '0' ? null : value
|
||||
},
|
||||
title: t('sideToolbar.workflows'),
|
||||
tooltip: t('sideToolbar.workflows'),
|
||||
component: markRaw(WorkflowsSidebarTab),
|
||||
type: 'vue'
|
||||
}
|
||||
}
|
||||
120
src/i18n.ts
120
src/i18n.ts
@@ -2,6 +2,7 @@ import { createI18n } from 'vue-i18n'
|
||||
|
||||
const messages = {
|
||||
en: {
|
||||
videoFailedToLoad: 'Video failed to load',
|
||||
extensionName: 'Extension Name',
|
||||
reloadToApplyChanges: 'Reload to apply changes',
|
||||
insert: 'Insert',
|
||||
@@ -48,7 +49,6 @@ const messages = {
|
||||
noResultsFound: 'No Results Found',
|
||||
searchFailedMessage:
|
||||
"We couldn't find any settings matching your search. Try adjusting your search terms.",
|
||||
noContent: '(No Content)',
|
||||
noTasksFound: 'No Tasks Found',
|
||||
noTasksFoundMessage: 'There are no tasks in the queue.',
|
||||
newFolder: 'New Folder',
|
||||
@@ -85,6 +85,7 @@ const messages = {
|
||||
change: 'On Change',
|
||||
changeTooltip: 'The workflow will be queued once a change is made',
|
||||
queueWorkflow: 'Queue workflow',
|
||||
queueWorkflowFront: 'Queue workflow (Insert at Front)',
|
||||
queue: 'Queue',
|
||||
interrupt: 'Cancel current run',
|
||||
refresh: 'Refresh node definitions',
|
||||
@@ -111,6 +112,7 @@ const messages = {
|
||||
}
|
||||
},
|
||||
zh: {
|
||||
videoFailedToLoad: '视频加载失败',
|
||||
extensionName: '扩展名称',
|
||||
reloadToApplyChanges: '重新加载以应用更改',
|
||||
insert: '插入',
|
||||
@@ -144,7 +146,7 @@ const messages = {
|
||||
delete: '删除',
|
||||
rename: '重命名',
|
||||
customize: '定制',
|
||||
experimental: '测试',
|
||||
experimental: 'BETA',
|
||||
deprecated: '弃用',
|
||||
loadWorkflow: '加载工作流',
|
||||
goToNode: '前往节点',
|
||||
@@ -192,6 +194,7 @@ const messages = {
|
||||
change: '变动',
|
||||
changeTooltip: '工作流将会在改变后执行',
|
||||
queueWorkflow: '执行工作流',
|
||||
queueWorkflowFront: '执行工作流 (队列首)',
|
||||
queue: '队列',
|
||||
interrupt: '取消当前任务',
|
||||
refresh: '刷新节点',
|
||||
@@ -216,6 +219,119 @@ const messages = {
|
||||
panMode: '平移模式',
|
||||
toggleLinkVisibility: '切换链接可见性'
|
||||
}
|
||||
},
|
||||
ru: {
|
||||
videoFailedToLoad: 'Видео не удалось загрузить',
|
||||
extensionName: 'Название расширения',
|
||||
reloadToApplyChanges: 'Перезагрузите, чтобы применить изменения',
|
||||
insert: 'Вставить',
|
||||
systemInfo: 'Информация о системе',
|
||||
devices: 'Устройства',
|
||||
about: 'О',
|
||||
add: 'Добавить',
|
||||
confirm: 'Подтвердить',
|
||||
reset: 'Сбросить',
|
||||
resetKeybindingsTooltip: 'Сбросить сочетания клавиш по умолчанию',
|
||||
customizeFolder: 'Настроить папку',
|
||||
icon: 'Иконка',
|
||||
color: 'Цвет',
|
||||
bookmark: 'Закладка',
|
||||
folder: 'Папка',
|
||||
star: 'Звёздочка',
|
||||
heart: 'Сердце',
|
||||
file: 'Файл',
|
||||
inbox: 'Входящие',
|
||||
box: 'Ящик',
|
||||
briefcase: 'Чемодан',
|
||||
error: 'Ошибка',
|
||||
loading: 'Загрузка',
|
||||
findIssues: 'Найти Issue',
|
||||
copyToClipboard: 'Копировать в буфер обмена',
|
||||
openNewIssue: 'Открыть новый Issue',
|
||||
showReport: 'Показать отчёт',
|
||||
imageFailedToLoad: 'Изображение не удалось загрузить',
|
||||
reconnecting: 'Переподключение',
|
||||
reconnected: 'Переподключено',
|
||||
delete: 'Удалить',
|
||||
rename: 'Переименовать',
|
||||
customize: 'Настроить',
|
||||
experimental: 'БЕТА',
|
||||
deprecated: 'УСТАР',
|
||||
loadWorkflow: 'Загрузить рабочий процесс',
|
||||
goToNode: 'Перейти к узлу',
|
||||
settings: 'Настройки',
|
||||
searchWorkflows: 'Поиск рабочих процессов',
|
||||
searchSettings: 'Поиск настроек',
|
||||
searchNodes: 'Поиск узлов',
|
||||
searchModels: 'Поиск моделей',
|
||||
searchKeybindings: 'Поиск сочетаний клавиш',
|
||||
noResultsFound: 'Ничего не найдено',
|
||||
searchFailedMessage:
|
||||
'Не удалось найти ни одной настройки, соответствующей вашему запросу. Попробуйте скорректировать поисковый запрос.',
|
||||
noContent: '(Нет контента)',
|
||||
noTasksFound: 'Задачи не найдены',
|
||||
noTasksFoundMessage: 'В очереди нет задач.',
|
||||
newFolder: 'Новая папка',
|
||||
sideToolbar: {
|
||||
themeToggle: 'Переключить тему',
|
||||
queue: 'Очередь',
|
||||
nodeLibrary: 'Библиотека узлов',
|
||||
workflows: 'Рабочие процессы',
|
||||
browseTemplates: 'Просмотреть примеры шаблонов',
|
||||
openWorkflow: 'Открыть рабочий процесс в локальной файловой системе',
|
||||
newBlankWorkflow: 'Создайте новый пустой рабочий процесс',
|
||||
nodeLibraryTab: {
|
||||
sortOrder: 'Порядок сортировки'
|
||||
},
|
||||
modelLibrary: 'Библиотека моделей',
|
||||
queueTab: {
|
||||
showFlatList: 'Показать плоский список',
|
||||
backToAllTasks: 'Вернуться ко всем задачам',
|
||||
containImagePreview: 'Предпросмотр заливающего изображения',
|
||||
coverImagePreview: 'Предпросмотр подходящего изображения',
|
||||
clearPendingTasks: 'Очистить отложенные задачи'
|
||||
}
|
||||
},
|
||||
menu: {
|
||||
batchCount: 'Количество пакетов',
|
||||
batchCountTooltip:
|
||||
'Количество раз, когда генерация рабочего процесса должна быть помещена в очередь',
|
||||
autoQueue: 'Автоочередь',
|
||||
disabled: 'Отключено',
|
||||
disabledTooltip:
|
||||
'Рабочий процесс не будет автоматически помещён в очередь',
|
||||
instant: 'Мгновенно',
|
||||
instantTooltip:
|
||||
'Рабочий процесс будет помещён в очередь сразу же после завершения генерации',
|
||||
change: 'При изменении',
|
||||
changeTooltip:
|
||||
'Рабочий процесс будет поставлен в очередь после внесения изменений',
|
||||
queueWorkflow: 'Очередь рабочего процесса',
|
||||
queueWorkflowFront: 'Очередь рабочего процесса (Вставка спереди)',
|
||||
queue: 'Очередь',
|
||||
interrupt: 'Отменить текущее выполнение',
|
||||
refresh: 'Обновить определения узлов',
|
||||
clipspace: 'Открыть Clipspace',
|
||||
resetView: 'Сбросить вид холста',
|
||||
clear: 'Очистить рабочий процесс'
|
||||
},
|
||||
templateWorkflows: {
|
||||
title: 'Начните работу с шаблона',
|
||||
template: {
|
||||
default: 'Image Generation',
|
||||
image2image: 'Image to Image',
|
||||
upscale: '2 Pass Upscale',
|
||||
flux_schnell: 'Flux Schnell'
|
||||
}
|
||||
},
|
||||
graphCanvasMenu: {
|
||||
zoomIn: 'Увеличить',
|
||||
zoomOut: 'Уменьшить',
|
||||
resetView: 'Сбросить вид',
|
||||
selectMode: 'Выбрать режим',
|
||||
panMode: 'Режим панорамирования',
|
||||
toggleLinkVisibility: 'Переключить видимость ссылок'
|
||||
}
|
||||
}
|
||||
// TODO: Add more languages
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import App from './App.vue'
|
||||
import router from '@/router'
|
||||
import { createApp } from 'vue'
|
||||
@@ -9,7 +10,6 @@ import Aura from '@primevue/themes/aura'
|
||||
import ConfirmationService from 'primevue/confirmationservice'
|
||||
import ToastService from 'primevue/toastservice'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import 'reflect-metadata'
|
||||
|
||||
import '@comfyorg/litegraph/style.css'
|
||||
import '@/assets/css/style.css'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { ComfyWorkflowJSON } from '@/types/comfyWorkflow'
|
||||
import {
|
||||
DownloadModelStatus,
|
||||
@@ -273,9 +274,15 @@ class ComfyApi extends EventTarget {
|
||||
* Loads node object definitions for the graph
|
||||
* @returns The node definitions
|
||||
*/
|
||||
async getNodeDefs(): Promise<Record<string, ComfyNodeDef>> {
|
||||
async getNodeDefs({ validate = false }: { validate?: boolean } = {}): Promise<
|
||||
Record<string, ComfyNodeDef>
|
||||
> {
|
||||
const resp = await this.fetchApi('/object_info', { cache: 'no-store' })
|
||||
const objectInfoUnsafe = await resp.json()
|
||||
if (!validate) {
|
||||
return objectInfoUnsafe
|
||||
}
|
||||
// Validate node definitions against zod schema. (slow)
|
||||
const objectInfo: Record<string, ComfyNodeDef> = {}
|
||||
for (const key in objectInfoUnsafe) {
|
||||
const validatedDef = validateComfyNodeDef(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { ComfyLogging } from './logging'
|
||||
import { ComfyWidgetConstructor, ComfyWidgets, initWidgets } from './widgets'
|
||||
import { ComfyUI, $el } from './ui'
|
||||
@@ -38,7 +39,7 @@ import {
|
||||
SYSTEM_NODE_DEFS,
|
||||
useNodeDefStore
|
||||
} from '@/stores/nodeDefStore'
|
||||
import { Vector2 } from '@comfyorg/litegraph'
|
||||
import { INodeInputSlot, Vector2 } from '@comfyorg/litegraph'
|
||||
import _ from 'lodash'
|
||||
import {
|
||||
showExecutionErrorDialog,
|
||||
@@ -53,6 +54,9 @@ import { useWorkspaceStore } from '@/stores/workspaceStateStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { IWidget } from '@comfyorg/litegraph'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import { KeyComboImpl, useKeybindingStore } from '@/stores/keybindingStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { shallowReactive } from 'vue'
|
||||
|
||||
export const ANIM_PREVIEW_WIDGET = '$$comfy_animation_preview'
|
||||
|
||||
@@ -111,7 +115,6 @@ export class ComfyApp {
|
||||
extensionManager: ExtensionManager
|
||||
_nodeOutputs: Record<string, any>
|
||||
nodePreviewImages: Record<string, typeof Image>
|
||||
shiftDown: boolean
|
||||
graph: LGraph
|
||||
enableWorkflowViewRestore: any
|
||||
canvas: LGraphCanvas
|
||||
@@ -137,12 +140,20 @@ export class ComfyApp {
|
||||
menu: ComfyAppMenu
|
||||
bypassBgColor: string
|
||||
|
||||
// @deprecated
|
||||
// Use useExecutionStore().executingNodeId instead
|
||||
/**
|
||||
* @deprecated Use useExecutionStore().executingNodeId instead
|
||||
*/
|
||||
get runningNodeId(): string | null {
|
||||
return useExecutionStore().executingNodeId
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use useWorkspaceStore().shiftDown instead
|
||||
*/
|
||||
get shiftDown(): boolean {
|
||||
return useWorkspaceStore().shiftDown
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.vueAppReady = false
|
||||
this.ui = new ComfyUI(this)
|
||||
@@ -175,12 +186,6 @@ export class ComfyApp {
|
||||
* @type {Record<string, Image>}
|
||||
*/
|
||||
this.nodePreviewImages = {}
|
||||
|
||||
/**
|
||||
* If the shift key on the keyboard is pressed
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.shiftDown = false
|
||||
}
|
||||
|
||||
get nodeOutputs() {
|
||||
@@ -1278,13 +1283,10 @@ export class ComfyApp {
|
||||
|
||||
/**
|
||||
* Handle keypress
|
||||
*
|
||||
* Ctrl + M mute/unmute selected nodes
|
||||
*/
|
||||
#addProcessKeyHandler() {
|
||||
const self = this
|
||||
const origProcessKey = LGraphCanvas.prototype.processKey
|
||||
LGraphCanvas.prototype.processKey = function (e) {
|
||||
LGraphCanvas.prototype.processKey = function (e: KeyboardEvent) {
|
||||
if (!this.graph) {
|
||||
return
|
||||
}
|
||||
@@ -1296,54 +1298,11 @@ export class ComfyApp {
|
||||
}
|
||||
|
||||
if (e.type == 'keydown' && !e.repeat) {
|
||||
// Ctrl + M mute/unmute
|
||||
if (e.key === 'm' && (e.metaKey || e.ctrlKey)) {
|
||||
if (this.selected_nodes) {
|
||||
for (var i in this.selected_nodes) {
|
||||
if (this.selected_nodes[i].mode === 2) {
|
||||
// never
|
||||
this.selected_nodes[i].mode = 0 // always
|
||||
} else {
|
||||
this.selected_nodes[i].mode = 2 // never
|
||||
}
|
||||
}
|
||||
}
|
||||
block_default = true
|
||||
}
|
||||
|
||||
// Ctrl + B bypass
|
||||
if (e.key === 'b' && (e.metaKey || e.ctrlKey)) {
|
||||
if (this.selected_nodes) {
|
||||
for (var i in this.selected_nodes) {
|
||||
if (this.selected_nodes[i].mode === 4) {
|
||||
// never
|
||||
this.selected_nodes[i].mode = 0 // always
|
||||
} else {
|
||||
this.selected_nodes[i].mode = 4 // never
|
||||
}
|
||||
}
|
||||
}
|
||||
block_default = true
|
||||
}
|
||||
|
||||
// p pin/unpin
|
||||
if (e.key === 'p') {
|
||||
if (this.selected_nodes) {
|
||||
for (const i in this.selected_nodes) {
|
||||
const node = this.selected_nodes[i]
|
||||
node.pin()
|
||||
}
|
||||
}
|
||||
block_default = true
|
||||
}
|
||||
|
||||
// Alt + C collapse/uncollapse
|
||||
if (e.key === 'c' && e.altKey) {
|
||||
if (this.selected_nodes) {
|
||||
for (var i in this.selected_nodes) {
|
||||
this.selected_nodes[i].collapse()
|
||||
}
|
||||
}
|
||||
const keyCombo = KeyComboImpl.fromEvent(e)
|
||||
const keybindingStore = useKeybindingStore()
|
||||
const keybinding = keybindingStore.getKeybinding(keyCombo)
|
||||
if (keybinding && keybinding.targetSelector === '#graph-canvas') {
|
||||
useCommandStore().execute(keybinding.commandId)
|
||||
block_default = true
|
||||
}
|
||||
|
||||
@@ -1362,26 +1321,6 @@ export class ComfyApp {
|
||||
// Trigger onPaste
|
||||
return true
|
||||
}
|
||||
|
||||
if (e.key === '+' && e.altKey) {
|
||||
block_default = true
|
||||
let scale = this.ds.scale * 1.1
|
||||
this.ds.changeScale(scale, [
|
||||
this.ds.element.width / 2,
|
||||
this.ds.element.height / 2
|
||||
])
|
||||
this.graph.change()
|
||||
}
|
||||
|
||||
if (e.key === '-' && e.altKey) {
|
||||
block_default = true
|
||||
let scale = (this.ds.scale * 1) / 1.1
|
||||
this.ds.changeScale(scale, [
|
||||
this.ds.element.width / 2,
|
||||
this.ds.element.height / 2
|
||||
])
|
||||
this.graph.change()
|
||||
}
|
||||
}
|
||||
|
||||
this.graph.change()
|
||||
@@ -1681,15 +1620,6 @@ export class ComfyApp {
|
||||
api.init()
|
||||
}
|
||||
|
||||
#addKeyboardHandler() {
|
||||
window.addEventListener('keydown', (e) => {
|
||||
this.shiftDown = e.shiftKey
|
||||
})
|
||||
window.addEventListener('keyup', (e) => {
|
||||
this.shiftDown = e.shiftKey
|
||||
})
|
||||
}
|
||||
|
||||
#addConfigureHandler() {
|
||||
const app = this
|
||||
const configure = LGraph.prototype.configure
|
||||
@@ -1908,7 +1838,20 @@ export class ComfyApp {
|
||||
|
||||
this.#addAfterConfigureHandler()
|
||||
|
||||
this.canvas = new LGraphCanvas(canvasEl, this.graph)
|
||||
// Make LGraphCanvas shallow reactive so that any change on the root object
|
||||
// triggers reactivity.
|
||||
this.canvas = shallowReactive(
|
||||
new LGraphCanvas(canvasEl, this.graph, {
|
||||
skip_events: true,
|
||||
skip_render: true
|
||||
})
|
||||
)
|
||||
// Bind event/ start rendering later, so that event handlers get reactive canvas reference.
|
||||
this.canvas.options.skip_events = false
|
||||
this.canvas.options.skip_render = false
|
||||
this.canvas.bindEvents()
|
||||
this.canvas.startRendering()
|
||||
|
||||
this.ctx = canvasEl.getContext('2d')
|
||||
|
||||
LiteGraph.alt_drag_do_clone_nodes = true
|
||||
@@ -1970,7 +1913,6 @@ export class ComfyApp {
|
||||
this.#addDropHandler()
|
||||
this.#addCopyHandler()
|
||||
this.#addPasteHandler()
|
||||
this.#addKeyboardHandler()
|
||||
this.#addWidgetLinkHandling()
|
||||
|
||||
await this.#invokeExtensionsAsync('setup')
|
||||
@@ -2027,7 +1969,9 @@ export class ComfyApp {
|
||||
*/
|
||||
async registerNodes() {
|
||||
// Load node definitions from the backend
|
||||
const defs = await api.getNodeDefs()
|
||||
const defs = await api.getNodeDefs({
|
||||
validate: useSettingStore().get('Comfy.Validation.NodeDefs')
|
||||
})
|
||||
await this.registerNodesFromDefs(defs)
|
||||
await this.#invokeExtensionsAsync('registerCustomNodes')
|
||||
if (this.vueAppReady) {
|
||||
@@ -2150,6 +2094,11 @@ export class ComfyApp {
|
||||
incoming: Record<string, any>
|
||||
) => {
|
||||
const result = { ...incoming }
|
||||
if (current.widget === undefined && incoming.widget !== undefined) {
|
||||
// Field must be input as only inputs can be converted
|
||||
this.inputs.push(current as INodeInputSlot)
|
||||
return incoming
|
||||
}
|
||||
for (const key of ['name', 'type', 'shape']) {
|
||||
if (current[key] !== undefined) {
|
||||
result[key] = current[key]
|
||||
@@ -2231,7 +2180,7 @@ export class ComfyApp {
|
||||
localStorage.setItem('litegrapheditor_clipboard', old)
|
||||
}
|
||||
|
||||
showMissingNodesError(missingNodeTypes, hasAddedNodes = true) {
|
||||
#showMissingNodesError(missingNodeTypes, hasAddedNodes = true) {
|
||||
if (useSettingStore().get('Comfy.Workflow.ShowMissingNodesWarning')) {
|
||||
showLoadWorkflowWarning({
|
||||
missingNodeTypes,
|
||||
@@ -2244,7 +2193,7 @@ export class ComfyApp {
|
||||
})
|
||||
}
|
||||
|
||||
showMissingModelsError(missingModels, paths) {
|
||||
#showMissingModelsError(missingModels, paths) {
|
||||
if (useSettingStore().get('Comfy.Workflow.ShowMissingModelsWarning')) {
|
||||
showMissingModelsWarning({
|
||||
missingModels,
|
||||
@@ -2483,11 +2432,11 @@ export class ComfyApp {
|
||||
|
||||
// TODO: Properly handle if both nodes and models are missing (sequential dialogs?)
|
||||
if (missingNodeTypes.length && showMissingNodesDialog) {
|
||||
this.showMissingNodesError(missingNodeTypes)
|
||||
this.#showMissingNodesError(missingNodeTypes)
|
||||
}
|
||||
if (missingModels.length && showMissingModelsDialog) {
|
||||
const paths = await api.getFolderPaths()
|
||||
this.showMissingModelsError(missingModels, paths)
|
||||
this.#showMissingModelsError(missingModels, paths)
|
||||
}
|
||||
await this.#invokeExtensionsAsync('afterConfigureGraph', missingNodeTypes)
|
||||
requestAnimationFrame(() => {
|
||||
@@ -2874,7 +2823,7 @@ export class ComfyApp {
|
||||
(n) => !LiteGraph.registered_node_types[n.class_type]
|
||||
)
|
||||
if (missingNodeTypes.length) {
|
||||
this.showMissingNodesError(
|
||||
this.#showMissingNodesError(
|
||||
// @ts-expect-error
|
||||
missingNodeTypes.map((t) => t.class_type),
|
||||
false
|
||||
@@ -2989,7 +2938,9 @@ export class ComfyApp {
|
||||
useModelStore().clearCache()
|
||||
}
|
||||
|
||||
const defs = await api.getNodeDefs()
|
||||
const defs = await api.getNodeDefs({
|
||||
validate: useSettingStore().get('Comfy.Validation.NodeDefs')
|
||||
})
|
||||
|
||||
for (const nodeId in defs) {
|
||||
this.registerNodeDef(nodeId, defs[nodeId])
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import type { ComfyApp } from './app'
|
||||
import { api } from './api'
|
||||
import { clone } from './utils'
|
||||
@@ -131,7 +132,11 @@ export class ChangeTracker {
|
||||
let keyIgnored = false
|
||||
window.addEventListener(
|
||||
'keydown',
|
||||
(e) => {
|
||||
(e: KeyboardEvent) => {
|
||||
// Do not trigger on repeat events (Holding down a key)
|
||||
// This can happen when user is holding down "Space" to pan the canvas.
|
||||
if (e.repeat) return
|
||||
|
||||
const activeEl = document.activeElement
|
||||
requestAnimationFrame(async () => {
|
||||
let bindInputEl
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app, ANIM_PREVIEW_WIDGET } from './app'
|
||||
import { LGraphCanvas, LGraphNode, LiteGraph } from '@comfyorg/litegraph'
|
||||
import type { Vector4 } from '@comfyorg/litegraph'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { $el, ComfyDialog } from './ui'
|
||||
import { api } from './api'
|
||||
import type { ComfyApp } from './app'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
export function getFromFlacBuffer(buffer: ArrayBuffer): Record<string, string> {
|
||||
const dataView = new DataView(buffer)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
export function getFromPngBuffer(buffer: ArrayBuffer) {
|
||||
// Get the PNG data as a Uint8Array
|
||||
const pngData = new Uint8Array(buffer)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import { api } from './api'
|
||||
import { getFromPngFile } from './metadata/png'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { api } from './api'
|
||||
import { ComfyDialog as _ComfyDialog } from './ui/dialog'
|
||||
import { toggleSwitch } from './ui/toggleSwitch'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { ComfyDialog } from '../dialog'
|
||||
import { $el } from '../../ui'
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { $el } from '../../ui'
|
||||
import { applyClasses, ClassList, toggleElement } from '../utils'
|
||||
import { prop } from '../../utils'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { $el } from '../../ui'
|
||||
import { ComfyButton } from './button'
|
||||
import { prop } from '../../utils'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { prop } from '../../utils'
|
||||
import { $el } from '../../ui'
|
||||
import { applyClasses, ClassList } from '../utils'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { $el } from '../ui'
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
/*
|
||||
Original implementation:
|
||||
https://github.com/TahaSh/drag-to-reorder
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../app'
|
||||
import { $el } from '../ui'
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { $el } from '../ui'
|
||||
import { api } from '../api'
|
||||
import { ComfyDialog } from './dialog'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { $el } from '../ui'
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { api } from '../api'
|
||||
import { $el } from '../ui'
|
||||
import { createSpinner } from './spinner'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
export type ClassList = string | string[] | Record<string, boolean>
|
||||
|
||||
export function applyClasses(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { api } from './api'
|
||||
import type { ComfyApp } from './app'
|
||||
import { $el } from './ui'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { api } from './api'
|
||||
import './domWidget'
|
||||
import type { ComfyApp } from './app'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import type { ComfyApp } from './app'
|
||||
import { api } from './api'
|
||||
import { ChangeTracker } from './changeTracker'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import Fuse, { IFuseOptions, FuseSearchOptions } from 'fuse.js'
|
||||
import _ from 'lodash'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '@/scripts/app'
|
||||
import { api } from '@/scripts/api'
|
||||
import { defineStore } from 'pinia'
|
||||
@@ -17,6 +18,7 @@ import { useTitleEditorStore } from './graphStore'
|
||||
import { useErrorHandling } from '@/hooks/errorHooks'
|
||||
import { useWorkflowStore } from './workflowStore'
|
||||
import { type KeybindingImpl, useKeybindingStore } from './keybindingStore'
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
export interface ComfyCommand {
|
||||
id: string
|
||||
@@ -75,6 +77,28 @@ export class ComfyCommandImpl implements ComfyCommand {
|
||||
const getTracker = () =>
|
||||
app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker
|
||||
|
||||
const getSelectedNodes = (): LGraphNode[] => {
|
||||
const selectedNodes = app.canvas.selected_nodes
|
||||
const result: LGraphNode[] = []
|
||||
if (selectedNodes) {
|
||||
for (const i in selectedNodes) {
|
||||
const node = selectedNodes[i]
|
||||
result.push(node)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const toggleSelectedNodesMode = (mode: number) => {
|
||||
getSelectedNodes().forEach((node) => {
|
||||
if (node.mode === mode) {
|
||||
node.mode = 0 // always
|
||||
} else {
|
||||
node.mode = mode
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const useCommandStore = defineStore('command', () => {
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
@@ -248,7 +272,11 @@ export const useCommandStore = defineStore('command', () => {
|
||||
icon: 'pi pi-plus',
|
||||
label: 'Zoom In',
|
||||
function: () => {
|
||||
app.canvas.ds.changeScale(app.canvas.ds.scale + 0.1)
|
||||
const ds = app.canvas.ds
|
||||
ds.changeScale(ds.scale * 1.1, [
|
||||
ds.element.width / 2,
|
||||
ds.element.height / 2
|
||||
])
|
||||
app.canvas.setDirty(true, true)
|
||||
}
|
||||
},
|
||||
@@ -257,7 +285,11 @@ export const useCommandStore = defineStore('command', () => {
|
||||
icon: 'pi pi-minus',
|
||||
label: 'Zoom Out',
|
||||
function: () => {
|
||||
app.canvas.ds.changeScale(app.canvas.ds.scale - 0.1)
|
||||
const ds = app.canvas.ds
|
||||
ds.changeScale(ds.scale / 1.1, [
|
||||
ds.element.width / 2,
|
||||
ds.element.height / 2
|
||||
])
|
||||
app.canvas.setDirty(true, true)
|
||||
}
|
||||
},
|
||||
@@ -365,6 +397,67 @@ export const useCommandStore = defineStore('command', () => {
|
||||
function: () => {
|
||||
useWorkflowStore().loadPreviousOpenedWorkflow()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.ToggleSelectedNodes.Mute',
|
||||
icon: 'pi pi-volume-off',
|
||||
label: 'Mute/Unmute Selected Nodes',
|
||||
versionAdded: '1.3.11',
|
||||
function: () => {
|
||||
toggleSelectedNodesMode(2) // muted
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.ToggleSelectedNodes.Bypass',
|
||||
icon: 'pi pi-shield',
|
||||
label: 'Bypass/Unbypass Selected Nodes',
|
||||
versionAdded: '1.3.11',
|
||||
function: () => {
|
||||
toggleSelectedNodesMode(4) // bypassed
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.ToggleSelectedNodes.Pin',
|
||||
icon: 'pi pi-pin',
|
||||
label: 'Pin/Unpin Selected Nodes',
|
||||
versionAdded: '1.3.11',
|
||||
function: () => {
|
||||
getSelectedNodes().forEach((node) => {
|
||||
node.pin(!node.pinned)
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.ToggleSelectedNodes.Collapse',
|
||||
icon: 'pi pi-minus',
|
||||
label: 'Collapse/Expand Selected Nodes',
|
||||
versionAdded: '1.3.11',
|
||||
function: () => {
|
||||
getSelectedNodes().forEach((node) => {
|
||||
node.collapse()
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.ToggleTheme',
|
||||
icon: 'pi pi-moon',
|
||||
label: 'Toggle Theme',
|
||||
versionAdded: '1.3.12',
|
||||
function: (() => {
|
||||
let previousDarkTheme: string = 'dark'
|
||||
|
||||
// Official light theme is the only light theme supported now.
|
||||
const isDarkMode = (themeId: string) => themeId !== 'light'
|
||||
return () => {
|
||||
const currentTheme = settingStore.get('Comfy.ColorPalette')
|
||||
if (isDarkMode(currentTheme)) {
|
||||
previousDarkTheme = currentTheme
|
||||
settingStore.set('Comfy.ColorPalette', 'light')
|
||||
} else {
|
||||
settingStore.set('Comfy.ColorPalette', previousDarkTheme)
|
||||
}
|
||||
}
|
||||
})()
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -94,5 +94,71 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Comfy.ShowSettingsDialog'
|
||||
},
|
||||
// For '=' both holding shift and not holding shift
|
||||
{
|
||||
combo: {
|
||||
key: '=',
|
||||
alt: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ZoomIn',
|
||||
targetSelector: '#graph-canvas'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: '+',
|
||||
alt: true,
|
||||
shift: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ZoomIn',
|
||||
targetSelector: '#graph-canvas'
|
||||
},
|
||||
// For number pad '+'
|
||||
{
|
||||
combo: {
|
||||
key: '+',
|
||||
alt: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ZoomIn',
|
||||
targetSelector: '#graph-canvas'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: '-',
|
||||
alt: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ZoomOut',
|
||||
targetSelector: '#graph-canvas'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'p'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Pin',
|
||||
targetSelector: '#graph-canvas'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'c',
|
||||
alt: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Collapse',
|
||||
targetSelector: '#graph-canvas'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'b',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Bypass',
|
||||
targetSelector: '#graph-canvas'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'm',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Mute',
|
||||
targetSelector: '#graph-canvas'
|
||||
}
|
||||
]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user