Merge remote-tracking branch 'origin/main' into feature/show_dropped_image_in_output_node

This commit is contained in:
bezo97
2024-10-13 15:43:01 +02:00
140 changed files with 2593 additions and 926 deletions

View File

@@ -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

View File

@@ -2,5 +2,6 @@
"singleQuote": true,
"tabWidth": 2,
"semi": false,
"trailingComma": "none"
"trailingComma": "none",
"printWidth": 80
}

View File

@@ -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
![image](https://github.com/user-attachments/assets/c84a1609-3880-48e0-a746-011f36beda68)
## Reset button
![image](https://github.com/user-attachments/assets/4d2922da-bb4f-4f90-8017-a8e4a0db07c7)
## Edit Keybinding
![image](https://github.com/user-attachments/assets/77626b7a-cb46-48f8-9465-e03120aac66a)
![image](https://github.com/user-attachments/assets/79131a4e-75c6-4715-bd11-c6aaed887779)
[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>
![GYEIRidb0AYGO-v](https://github.com/user-attachments/assets/e6cde0b6-654b-4afd-a117-133657a410b1)
</details>
<details>
<summary>v1.2.44: **Litegraph** Double click group title to edit</summary>

View File

@@ -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: {

View File

@@ -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

View 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
}

View File

@@ -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)
})
})
})

View 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'
)
}
}
}

View 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'])
}
}

View File

@@ -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)
})
})
})

View File

@@ -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()
})
})

View File

@@ -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)
})
})

View File

@@ -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()
})
})

View 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)
}
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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: [

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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())

View File

@@ -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>

View File

@@ -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'

View File

@@ -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'

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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));

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -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)

View File

@@ -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>

View 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>

View File

@@ -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<{

View File

@@ -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 = {}) => {

View File

@@ -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

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { app } from '../../scripts/app'
import { ComfyDialog, $el } from '../../scripts/ui'
import { ComfyApp } from '../../scripts/app'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { useToastStore } from '@/stores/toastStore'
import { app } from '../../scripts/app'
import { $el } from '../../scripts/ui'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { LiteGraph, LGraphCanvas } from '@comfyorg/litegraph'
import { app } from '../../scripts/app'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { app } from '../../scripts/app'
// Allows for simple dynamic prompt replacement

View File

@@ -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

View File

@@ -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()
},

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { $el, ComfyDialog } from '../../scripts/ui'
import { DraggableList } from '../../scripts/ui/draggableList'
import { GroupNodeConfig, GroupNodeHandler } from './groupNode'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { LGraphGroup } from '@comfyorg/litegraph'
import { app } from '../../scripts/app'
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { LiteGraph } from '@comfyorg/litegraph'
import { app } from '../../scripts/app'

View File

@@ -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

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { app } from '../../scripts/app'
import { LiteGraph } from '@comfyorg/litegraph'
const id = 'Comfy.LinkRenderMode'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { app } from '../../scripts/app'
import { ComfyDialog, $el } from '../../scripts/ui'
import { ComfyApp } from '../../scripts/app'

View File

@@ -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'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { app } from '../../scripts/app'
import { api } from '../../scripts/api'
import { ComfyDialog, $el } from '../../scripts/ui'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { LiteGraph, LGraphCanvas } from '@comfyorg/litegraph'
import { app } from '../../scripts/app'
import { ComfyWidgets } from '../../scripts/widgets'

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { app } from '../../scripts/app'
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { app } from '../../scripts/app'
import { ComfyWidgets } from '../../scripts/widgets'
import { LiteGraph } from '@comfyorg/litegraph'

View File

@@ -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)

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { app } from '../../scripts/app'
import { api } from '../../scripts/api'
import type { IWidget } from '@comfyorg/litegraph'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { app } from '../../scripts/app'
import { ComfyNodeDef } from '@/types/apiTypes'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { app } from '../../scripts/app'
import { api } from '../../scripts/api'
import { useToastStore } from '@/stores/toastStore'

View File

@@ -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
View 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()
})
}

View 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'
}
}

View 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'
}
}

View 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'
}
}

View 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'
}
}

View File

@@ -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
}

View File

@@ -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'

View File

@@ -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(

View File

@@ -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])

View File

@@ -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

View File

@@ -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'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { $el, ComfyDialog } from './ui'
import { api } from './api'
import type { ComfyApp } from './app'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
export function getFromFlacBuffer(buffer: ArrayBuffer): Record<string, string> {
const dataView = new DataView(buffer)

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
export function getFromPngBuffer(buffer: ArrayBuffer) {
// Get the PNG data as a Uint8Array
const pngData = new Uint8Array(buffer)

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { LiteGraph } from '@comfyorg/litegraph'
import { api } from './api'
import { getFromPngFile } from './metadata/png'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { api } from './api'
import { ComfyDialog as _ComfyDialog } from './ui/dialog'
import { toggleSwitch } from './ui/toggleSwitch'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { ComfyDialog } from '../dialog'
import { $el } from '../../ui'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { $el } from '../../ui'
import { applyClasses, ClassList, toggleElement } from '../utils'
import { prop } from '../../utils'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { $el } from '../../ui'
import { ComfyButton } from './button'
import { prop } from '../../utils'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { prop } from '../../utils'
import { $el } from '../../ui'
import { applyClasses, ClassList } from '../utils'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { useDialogStore } from '@/stores/dialogStore'
import { $el } from '../ui'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
/*
Original implementation:
https://github.com/TahaSh/drag-to-reorder

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { app } from '../app'
import { $el } from '../ui'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { $el } from '../ui'
import { api } from '../api'
import { ComfyDialog } from './dialog'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { $el } from '../ui'
/**

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { api } from '../api'
import { $el } from '../ui'
import { createSpinner } from './spinner'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
export type ClassList = string | string[] | Record<string, boolean>
export function applyClasses(

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { api } from './api'
import type { ComfyApp } from './app'
import { $el } from './ui'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { api } from './api'
import './domWidget'
import type { ComfyApp } from './app'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import type { ComfyApp } from './app'
import { api } from './api'
import { ChangeTracker } from './changeTracker'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
import Fuse, { IFuseOptions, FuseSearchOptions } from 'fuse.js'
import _ from 'lodash'

View File

@@ -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)
}
}
})()
}
]

View File

@@ -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