Compare commits

..

2 Commits

Author SHA1 Message Date
christian-byrne
11c46ea62d Refactor Comfy version retrieval and update changelog version description 2025-01-18 13:04:22 -07:00
christian-byrne
86789ceb9d Show changelog on new version 2025-01-18 12:25:04 -07:00
107 changed files with 2627 additions and 5131 deletions

3
.gitignore vendored
View File

@@ -16,9 +16,6 @@ dist-ssr
.vscode/*
*.code-workspace
!.vscode/extensions.json
!.vscode/tailwind.json
!.vscode/settings.json.default
!.vscode/launch.json.default
.idea
.DS_Store
*.suo

View File

@@ -1,16 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome on frontend dev",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/src",
"sourceMaps": true,
}
]
}

View File

@@ -1,5 +0,0 @@
{
"css.customData": [
".vscode/tailwind.json"
]
}

55
.vscode/tailwind.json vendored
View File

@@ -1,55 +0,0 @@
{
"version": 1.1,
"atDirectives": [
{
"name": "@tailwind",
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
}
]
},
{
"name": "@apply",
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that youd like to extract to a new component.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#apply"
}
]
},
{
"name": "@responsive",
"description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
}
]
},
{
"name": "@screen",
"description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#screen"
}
]
},
{
"name": "@variants",
"description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#variants"
}
]
}
]
}

View File

@@ -1,4 +1,4 @@
import { Locator, expect } from '@playwright/test'
import { expect } from '@playwright/test'
import { Keybinding } from '../src/types/keyBindingTypes'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
@@ -66,71 +66,9 @@ test.describe('Missing models warning', () => {
}, comfyPage.url)
})
test('Should display a warning when missing models are found', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('missing_models')
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).toBeVisible()
const downloadButton = missingModelsWarning.getByLabel('Download')
await expect(downloadButton).toBeVisible()
})
test('Should not display a warning when no missing models are found', async ({
comfyPage
}) => {
const modelFoldersRes = {
status: 200,
body: JSON.stringify([
{
name: 'clip',
folders: ['ComfyUI/models/clip']
}
])
}
comfyPage.page.route(
'**/api/experiment/models',
(route) => route.fulfill(modelFoldersRes),
{ times: 1 }
)
// Reload page to trigger indexing of model folders
await comfyPage.setup()
const clipModelsRes = {
status: 200,
body: JSON.stringify([
{
name: 'fake_model.safetensors',
pathIndex: 0
}
])
}
comfyPage.page.route(
'**/api/experiment/models/clip',
(route) => route.fulfill(clipModelsRes),
{ times: 1 }
)
await comfyPage.loadWorkflow('missing_models')
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).not.toBeVisible()
})
test('should show on tutorial workflow', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.TutorialCompleted', false)
await comfyPage.setup({ clearStorage: true })
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).toBeVisible()
expect(await comfyPage.getSetting('Comfy.TutorialCompleted')).toBe(true)
})
// Flaky test after parallelization
// https://github.com/Comfy-Org/ComfyUI_frontend/pull/1400
test.skip('Should download missing model when clicking download button', async ({
test.skip('Should display a warning when missing models are found', async ({
comfyPage
}) => {
// The fake_model.safetensors is served by
@@ -148,49 +86,6 @@ test.describe('Missing models warning', () => {
const download = await downloadPromise
expect(download.suggestedFilename()).toBe('fake_model.safetensors')
})
test.describe('Do not show again checkbox', () => {
let checkbox: Locator
let closeButton: Locator
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting(
'Comfy.Workflow.ShowMissingModelsWarning',
true
)
await comfyPage.loadWorkflow('missing_models')
checkbox = comfyPage.page.getByLabel("Don't show this again")
closeButton = comfyPage.page.getByLabel('Close')
})
test('Should disable warning dialog when checkbox is checked', async ({
comfyPage
}) => {
await checkbox.click()
const changeSettingPromise = comfyPage.page.waitForRequest(
'**/api/settings/Comfy.Workflow.ShowMissingModelsWarning'
)
await closeButton.click()
await changeSettingPromise
const settingValue = await comfyPage.getSetting(
'Comfy.Workflow.ShowMissingModelsWarning'
)
expect(settingValue).toBe(false)
})
test('Should keep warning dialog enabled when checkbox is unchecked', async ({
comfyPage
}) => {
await closeButton.click()
const settingValue = await comfyPage.getSetting(
'Comfy.Workflow.ShowMissingModelsWarning'
)
expect(settingValue).toBe(true)
})
})
})
test.describe('Settings', () => {
@@ -270,37 +165,3 @@ test.describe('Settings', () => {
expect(request.postData()).toContain(JSON.stringify(expectedSetting))
})
})
test.describe('Feedback dialog', () => {
test('Should open from topmenu help command', async ({ comfyPage }) => {
// Open feedback dialog from top menu
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])
// Verify feedback dialog content is visible
const feedbackHeader = comfyPage.page.getByRole('heading', {
name: 'Feedback'
})
await expect(feedbackHeader).toBeVisible()
})
test('Should close when close button clicked', async ({ comfyPage }) => {
// Open feedback dialog
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])
const feedbackHeader = comfyPage.page.getByRole('heading', {
name: 'Feedback'
})
// Close feedback dialog
await comfyPage.page
.getByLabel('', { exact: true })
.getByLabel('Close')
.click()
await feedbackHeader.waitFor({ state: 'hidden' })
// Verify dialog is closed
await expect(feedbackHeader).not.toBeVisible()
})
})

View File

@@ -904,9 +904,7 @@ export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
'Comfy.NodeBadge.NodeSourceBadgeMode': NodeBadgeMode.None,
// Disable tooltips by default to avoid flakiness.
'Comfy.EnableTooltips': false,
'Comfy.userId': userId,
// Set tutorial completed to true to avoid loading the tutorial workflow.
'Comfy.TutorialCompleted': true
'Comfy.userId': userId
})
} catch (e) {
console.error(e)

View File

@@ -82,14 +82,10 @@ test.describe('Node search box', () => {
test('Has correct aria-labels on search results', async ({ comfyPage }) => {
const node = 'Load Checkpoint'
await comfyPage.doubleClickCanvas()
await comfyPage.searchBox.input.waitFor({ state: 'visible' })
await comfyPage.searchBox.input.fill(node)
await comfyPage.searchBox.dropdown.waitFor({ state: 'visible' })
// Wait for some time for the auto complete list to update.
// The auto complete list is debounced and may take some time to update.
await comfyPage.page.waitForTimeout(500)
const firstResult = comfyPage.searchBox.dropdown.locator('li').first()
await comfyPage.searchBox.fillAndSelectFirstNode(node)
const firstResult = comfyPage.page
.locator('li.p-autocomplete-option')
.first()
await expect(firstResult).toHaveAttribute('aria-label', node)
})

View File

@@ -102,18 +102,6 @@ test.describe('Node Right Click Menu', () => {
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
await comfyPage.page.getByText('Convert Widget to Input').click()
await comfyPage.nextFrame()
// The submenu has an identical entry as the base menu - use last
await comfyPage.page.getByText('Convert width to input').last().click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-node-widget-converted.png'
)
})
test('Can convert widget without submenu', async ({ comfyPage }) => {
// Right-click the width widget
await comfyPage.rightClickEmptyLatentNode()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
await comfyPage.page.getByText('Convert width to input').click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 106 KiB

12
global.d.ts vendored
View File

@@ -1,15 +1,3 @@
declare const __COMFYUI_FRONTEND_VERSION__: string
declare const __SENTRY_ENABLED__: boolean
declare const __SENTRY_DSN__: string
interface Navigator {
/**
* Used by the electron API. This is a WICG non-standard API, but is guaranteed to exist in Electron.
* It is `undefined` in Firefox and older browsers.
* @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/windowControlsOverlay
*/
windowControlsOverlay?: {
/** When `true`, the window is using custom window style. */
visible: boolean
}
}

View File

@@ -11,7 +11,7 @@ export default {
function formatAndEslint(fileNames) {
return [
`prettier --write ${fileNames.join(' ')}`,
`prettier --write ${fileNames.join(' ')} --plugin @trivago/prettier-plugin-sort-imports`,
`eslint --fix ${fileNames.join(' ')}`
]
}

336
package-lock.json generated
View File

@@ -1,19 +1,18 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.8.8",
"version": "1.7.14",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@comfyorg/comfyui-frontend",
"version": "1.8.8",
"version": "1.7.14",
"license": "GPL-3.0-only",
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.11",
"@comfyorg/litegraph": "^0.8.62",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@comfyorg/comfyui-electron-types": "^0.4.10",
"@comfyorg/litegraph": "^0.8.60",
"@primevue/themes": "^4.0.5",
"@sentry/vue": "^8.48.0",
"@tiptap/core": "^2.10.4",
"@tiptap/extension-link": "^2.10.4",
@@ -33,10 +32,10 @@
"loglevel": "^1.9.2",
"pinia": "^2.1.7",
"primeicons": "^7.0.0",
"primevue": "^4.2.5",
"primevue": "^4.0.5",
"three": "^0.170.0",
"tiptap-markdown": "^0.8.10",
"vue": "^3.5.13",
"vue": "^3.4.31",
"vue-i18n": "^9.13.1",
"vue-router": "^4.4.3",
"zod": "^3.23.8",
@@ -1937,15 +1936,14 @@
"dev": true
},
"node_modules/@comfyorg/comfyui-electron-types": {
"version": "0.4.11",
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.11.tgz",
"integrity": "sha512-RGJeWwXjyv0Ojj7xkZKgcRxC1nFv1nh7qEWpNBiofxVgFiap9Ei79b/KJYxNE0no4BoYqRMaRg+sFtCE6yEukA==",
"license": "GPL-3.0-only"
"version": "0.4.10",
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.10.tgz",
"integrity": "sha512-UWBgyuWeV7vussYZVUYhCe0jj+XbIq2nglrCUy6IgFgXp9pbE8Ktg5D36WxE0RWj6SvVXErlCL9wWnMktaRbCA=="
},
"node_modules/@comfyorg/litegraph": {
"version": "0.8.62",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.62.tgz",
"integrity": "sha512-El2QO8m1ky0YB/tUTDA3sorhtzZgBItMZ+j80eO9fAQb/ptToTUId+DxWLAZyz0qhA7RXJFA27emX+sdtWYO9g==",
"version": "0.8.60",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.60.tgz",
"integrity": "sha512-LkZalBcka1xVxkL7JnkF/1EzyvspLyrSthzyN9ZumWJw7kAaZkO9omraXv2t/UiFsqwMr5M/AV5UY915Vq8cxQ==",
"license": "MIT"
},
"node_modules/@cspotcode/source-map-support": {
@@ -3962,129 +3960,63 @@
"node": ">=12"
}
},
"node_modules/@primeuix/forms": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@primeuix/forms/-/forms-0.0.2.tgz",
"integrity": "sha512-DpecPQd/Qf/kav4LKCaIeGuT3AkwhJzuHCkLANTVlN/zBvo8KIj3OZHsCkm0zlIMVVnaJdtx1ULNlRQdudef+A==",
"dependencies": {
"@primeuix/utils": "^0.3.0"
},
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/@primeuix/forms/node_modules/@primeuix/utils": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.3.2.tgz",
"integrity": "sha512-B+nphqTQeq+i6JuICLdVWnDMjONome2sNz0xI65qIOyeB4EF12CoKRiCsxuZ5uKAkHi/0d1LqlQ9mIWRSdkavw==",
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/@primeuix/styled": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.3.2.tgz",
"integrity": "sha512-ColZes0+/WKqH4ob2x8DyNYf1NENpe5ZguOvx5yCLxaP8EIMVhLjWLO/3umJiDnQU4XXMLkn2mMHHw+fhTX/mw==",
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.0.5.tgz",
"integrity": "sha512-pVoGn/uPkVm/DyF3TR3EmH/pL/dP4nR42FcYbVduFq9VfO3KVeOEqvcCULHXos66RZO9MCbCFUoLy6ctf9GUGQ==",
"license": "MIT",
"dependencies": {
"@primeuix/utils": "^0.3.2"
"@primeuix/utils": "^0.0.5"
},
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/@primeuix/utils": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.3.2.tgz",
"integrity": "sha512-B+nphqTQeq+i6JuICLdVWnDMjONome2sNz0xI65qIOyeB4EF12CoKRiCsxuZ5uKAkHi/0d1LqlQ9mIWRSdkavw==",
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.0.5.tgz",
"integrity": "sha512-ntUiUgtRtkF8KuaxHffzhYxQxoXk6LAPHm7CVlFjdqS8Rx8xRkLkZVyo84E+pO2hcNFkOGVP/GxHhQ2s94O8zA==",
"license": "MIT",
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/@primevue/core": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.2.5.tgz",
"integrity": "sha512-+oWBIQs5dLd2Ini4KEVOlvPILk989EHAskiFS3R/dz3jeOllJDMZFcSp8V9ddV0R3yDaPdLVkfHm2Q5t42kU2Q==",
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.0.5.tgz",
"integrity": "sha512-DUCslDA93eUOVW0A1I3yoZgRLI4zmI2++loZQXbUF5jaXCwKiAza14+iyUU+cWH27VSq+jQnCEP9QJtPZiJJ0w==",
"license": "MIT",
"dependencies": {
"@primeuix/styled": "^0.3.2",
"@primeuix/utils": "^0.3.2"
"@primeuix/styled": "^0.0.5",
"@primeuix/utils": "^0.0.5"
},
"engines": {
"node": ">=12.11.0"
},
"peerDependencies": {
"vue": "^3.3.0"
}
},
"node_modules/@primevue/forms": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/@primevue/forms/-/forms-4.2.5.tgz",
"integrity": "sha512-5jarJQ9Qv32bOo/0tY5bqR3JZI6+YmmoUQ2mjhVSbVElQsE4FNfhT7a7JwF+xgBPMPc8KWGNA1QB248HhPNVAg==",
"dependencies": {
"@primeuix/forms": "^0.0.2",
"@primeuix/utils": "^0.3.2",
"@primevue/core": "4.2.5"
},
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/@primevue/forms/node_modules/@primeuix/styled": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.3.2.tgz",
"integrity": "sha512-ColZes0+/WKqH4ob2x8DyNYf1NENpe5ZguOvx5yCLxaP8EIMVhLjWLO/3umJiDnQU4XXMLkn2mMHHw+fhTX/mw==",
"dependencies": {
"@primeuix/utils": "^0.3.2"
},
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/@primevue/forms/node_modules/@primeuix/utils": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.3.2.tgz",
"integrity": "sha512-B+nphqTQeq+i6JuICLdVWnDMjONome2sNz0xI65qIOyeB4EF12CoKRiCsxuZ5uKAkHi/0d1LqlQ9mIWRSdkavw==",
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/@primevue/forms/node_modules/@primevue/core": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.2.5.tgz",
"integrity": "sha512-+oWBIQs5dLd2Ini4KEVOlvPILk989EHAskiFS3R/dz3jeOllJDMZFcSp8V9ddV0R3yDaPdLVkfHm2Q5t42kU2Q==",
"dependencies": {
"@primeuix/styled": "^0.3.2",
"@primeuix/utils": "^0.3.2"
},
"engines": {
"node": ">=12.11.0"
},
"peerDependencies": {
"vue": "^3.3.0"
"vue": "^3.0.0"
}
},
"node_modules/@primevue/icons": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.2.5.tgz",
"integrity": "sha512-WFbUMZhQkXf/KmwcytkjGVeJ9aGEDXjP3uweOjQZMmRdEIxFnqYYpd90wE90JE1teZn3+TVnT4ZT7ejGyEXnFQ==",
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.0.5.tgz",
"integrity": "sha512-ZxR9W1wlAE2fTtUhrHyeMx5t0jNyAgxDcHPm0cNXpX8q1XF95rSM/qb48QKXIBDBrJ/xs57BcyCNADP/VDPY4g==",
"license": "MIT",
"dependencies": {
"@primeuix/utils": "^0.3.2",
"@primevue/core": "4.2.5"
"@primeuix/utils": "^0.0.5",
"@primevue/core": "4.0.5"
},
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/@primevue/themes": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/@primevue/themes/-/themes-4.2.5.tgz",
"integrity": "sha512-8F7yA36xYIKtNuAuyBdZZEks/bKDwlhH5WjpqGGB0FdwfAEoBYsynQ5sdqcT2Lb/NsajHmS5lc++Ttlvr1g1Lw==",
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@primevue/themes/-/themes-4.0.5.tgz",
"integrity": "sha512-cRrAhOapOT8eFCTDwNdB/acg2ZEEkn7y6h6p188PYSjJsWnYK+D8eI1Js1ZB5HwWo4sWs3oR3Sy8bPwejnGbAw==",
"license": "MIT",
"dependencies": {
"@primeuix/styled": "^0.3.2"
"@primeuix/styled": "^0.0.5"
},
"engines": {
"node": ">=12.11.0"
@@ -5793,53 +5725,49 @@
}
},
"node_modules/@vue/compiler-core": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
"license": "MIT",
"version": "3.4.31",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.31.tgz",
"integrity": "sha512-skOiodXWTV3DxfDhB4rOf3OGalpITLlgCeOwb+Y9GJpfQ8ErigdBUHomBzvG78JoVE8MJoQsb+qhZiHfKeNeEg==",
"dependencies": {
"@babel/parser": "^7.25.3",
"@vue/shared": "3.5.13",
"@babel/parser": "^7.24.7",
"@vue/shared": "3.4.31",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
"license": "MIT",
"version": "3.4.31",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.31.tgz",
"integrity": "sha512-wK424WMXsG1IGMyDGyLqB+TbmEBFM78hIsOJ9QwUVLGrcSk0ak6zYty7Pj8ftm7nEtdU/DGQxAXp0/lM/2cEpQ==",
"dependencies": {
"@vue/compiler-core": "3.5.13",
"@vue/shared": "3.5.13"
"@vue/compiler-core": "3.4.31",
"@vue/shared": "3.4.31"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
"integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
"license": "MIT",
"version": "3.4.31",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.31.tgz",
"integrity": "sha512-einJxqEw8IIJxzmnxmJBuK2usI+lJonl53foq+9etB2HAzlPjAS/wa7r0uUpXw5ByX3/0uswVSrjNb17vJm1kQ==",
"dependencies": {
"@babel/parser": "^7.25.3",
"@vue/compiler-core": "3.5.13",
"@vue/compiler-dom": "3.5.13",
"@vue/compiler-ssr": "3.5.13",
"@vue/shared": "3.5.13",
"@babel/parser": "^7.24.7",
"@vue/compiler-core": "3.4.31",
"@vue/compiler-dom": "3.4.31",
"@vue/compiler-ssr": "3.4.31",
"@vue/shared": "3.4.31",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.11",
"postcss": "^8.4.48",
"magic-string": "^0.30.10",
"postcss": "^8.4.38",
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
"integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
"license": "MIT",
"version": "3.4.31",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.31.tgz",
"integrity": "sha512-RtefmITAje3fJ8FSg1gwgDhdKhZVntIVbwupdyZDSifZTRMiWxWehAOTCc8/KZDnBOcYQ4/9VWxsTbd3wT0hAA==",
"dependencies": {
"@vue/compiler-dom": "3.5.13",
"@vue/shared": "3.5.13"
"@vue/compiler-dom": "3.4.31",
"@vue/shared": "3.4.31"
}
},
"node_modules/@vue/compiler-vue2": {
@@ -5883,6 +5811,38 @@
}
}
},
"node_modules/@vue/language-core/node_modules/@vue/compiler-core": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.25.3",
"@vue/shared": "3.5.13",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/language-core/node_modules/@vue/compiler-dom": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"node_modules/@vue/language-core/node_modules/@vue/shared": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@vue/language-core/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@@ -5910,54 +5870,49 @@
}
},
"node_modules/@vue/reactivity": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
"integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
"license": "MIT",
"version": "3.4.31",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.31.tgz",
"integrity": "sha512-VGkTani8SOoVkZNds1PfJ/T1SlAIOf8E58PGAhIOUDYPC4GAmFA2u/E14TDAFcf3vVDKunc4QqCe/SHr8xC65Q==",
"dependencies": {
"@vue/shared": "3.5.13"
"@vue/shared": "3.4.31"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
"integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
"license": "MIT",
"version": "3.4.31",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.31.tgz",
"integrity": "sha512-LDkztxeUPazxG/p8c5JDDKPfkCDBkkiNLVNf7XZIUnJ+66GVGkP+TIh34+8LtPisZ+HMWl2zqhIw0xN5MwU1cw==",
"dependencies": {
"@vue/reactivity": "3.5.13",
"@vue/shared": "3.5.13"
"@vue/reactivity": "3.4.31",
"@vue/shared": "3.4.31"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
"integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
"license": "MIT",
"version": "3.4.31",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.31.tgz",
"integrity": "sha512-2Auws3mB7+lHhTFCg8E9ZWopA6Q6L455EcU7bzcQ4x6Dn4cCPuqj6S2oBZgN2a8vJRS/LSYYxwFFq2Hlx3Fsaw==",
"dependencies": {
"@vue/reactivity": "3.5.13",
"@vue/runtime-core": "3.5.13",
"@vue/shared": "3.5.13",
"@vue/reactivity": "3.4.31",
"@vue/runtime-core": "3.4.31",
"@vue/shared": "3.4.31",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
"integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
"license": "MIT",
"version": "3.4.31",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.31.tgz",
"integrity": "sha512-D5BLbdvrlR9PE3by9GaUp1gQXlCNadIZytMIb8H2h3FMWJd4oUfkUTEH2wAr3qxoRz25uxbTcbqd3WKlm9EHQA==",
"dependencies": {
"@vue/compiler-ssr": "3.5.13",
"@vue/shared": "3.5.13"
"@vue/compiler-ssr": "3.4.31",
"@vue/shared": "3.4.31"
},
"peerDependencies": {
"vue": "3.5.13"
"vue": "3.4.31"
}
},
"node_modules/@vue/shared": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
"license": "MIT"
"version": "3.4.31",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.31.tgz",
"integrity": "sha512-Yp3wtJk//8cO4NItOPpi3QkLExAr/aLBGZMmTtW9WpdwBCJpRM6zj9WgWktXAl8IDIozwNMByT45JP3tO3ACWA=="
},
"node_modules/@vue/test-utils": {
"version": "2.4.6",
@@ -7891,8 +7846,7 @@
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/data-urls": {
"version": "3.0.2",
@@ -14472,16 +14426,15 @@
}
},
"node_modules/nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -15216,10 +15169,9 @@
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -15383,9 +15335,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"funding": [
{
"type": "opencollective",
@@ -15400,10 +15352,9 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
"nanoid": "^3.3.7",
"picocolors": "^1.1.0",
"source-map-js": "^1.2.1"
},
"engines": {
@@ -15576,15 +15527,15 @@
"integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw=="
},
"node_modules/primevue": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.2.5.tgz",
"integrity": "sha512-7UMOIJvdFz4jQyhC76yhNdSlHtXvVpmE2JSo2ndUTBWjWJOkYyT562rQ4ayO+bMdJLtzBGqgY64I9ZfEvNd7vQ==",
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.0.5.tgz",
"integrity": "sha512-MALszGIZ5SnEQy1XeZLBFhpMXQ1OS7D1U7H+l/JAX5U46RQ1vufo7NAiWbbV5/ADjPGw4uLplqMQxujkksNY2g==",
"license": "MIT",
"dependencies": {
"@primeuix/styled": "^0.3.2",
"@primeuix/utils": "^0.3.2",
"@primevue/core": "4.2.5",
"@primevue/icons": "4.2.5"
"@primeuix/styled": "^0.0.5",
"@primeuix/utils": "^0.0.5",
"@primevue/core": "4.0.5",
"@primevue/icons": "4.0.5"
},
"engines": {
"node": ">=12.11.0"
@@ -18864,16 +18815,15 @@
"license": "MIT"
},
"node_modules/vue": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
"license": "MIT",
"version": "3.4.31",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.31.tgz",
"integrity": "sha512-njqRrOy7W3YLAlVqSKpBebtZpDVg21FPoaq1I7f/+qqBThK9ChAIjkRWgeP6Eat+8C+iia4P3OYqpATP21BCoQ==",
"dependencies": {
"@vue/compiler-dom": "3.5.13",
"@vue/compiler-sfc": "3.5.13",
"@vue/runtime-dom": "3.5.13",
"@vue/server-renderer": "3.5.13",
"@vue/shared": "3.5.13"
"@vue/compiler-dom": "3.4.31",
"@vue/compiler-sfc": "3.4.31",
"@vue/runtime-dom": "3.4.31",
"@vue/server-renderer": "3.4.31",
"@vue/shared": "3.4.31"
},
"peerDependencies": {
"typescript": "*"

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.8.8",
"version": "1.7.14",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -83,10 +83,9 @@
},
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.11",
"@comfyorg/litegraph": "^0.8.62",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@comfyorg/comfyui-electron-types": "^0.4.10",
"@comfyorg/litegraph": "^0.8.60",
"@primevue/themes": "^4.0.5",
"@sentry/vue": "^8.48.0",
"@tiptap/core": "^2.10.4",
"@tiptap/extension-link": "^2.10.4",
@@ -106,10 +105,10 @@
"loglevel": "^1.9.2",
"pinia": "^2.1.7",
"primeicons": "^7.0.0",
"primevue": "^4.2.5",
"primevue": "^4.0.5",
"three": "^0.170.0",
"tiptap-markdown": "^0.8.10",
"vue": "^3.5.13",
"vue": "^3.4.31",
"vue-i18n": "^9.13.1",
"vue-router": "^4.4.3",
"zod": "^3.23.8",

View File

@@ -266,7 +266,7 @@
],
"properties": {},
"widgets_values": [
"v1-5-pruned-emaonly-fp16.safetensors"
"v1-5-pruned-emaonly.safetensors"
]
}
],
@@ -349,8 +349,8 @@
"extra": {},
"version": 0.4,
"models": [{
"name": "v1-5-pruned-emaonly-fp16.safetensors",
"url": "https://huggingface.co/Comfy-Org/stable-diffusion-v1-5-archive/resolve/main/v1-5-pruned-emaonly-fp16.safetensors?download=true",
"name": "v1-5-pruned-emaonly.safetensors",
"url": "https://huggingface.co/Comfy-Org/stable-diffusion-v1-5-archive/resolve/main/v1-5-pruned-emaonly.safetensors?download=true",
"directory": "checkpoints"
}]
}

View File

@@ -1,43 +0,0 @@
import { execSync } from 'child_process'
import { readFileSync } from 'fs'
const packageName = '@comfyorg/comfyui-electron-types'
const description = 'desktop API types'
try {
// Create a new branch
console.log('Creating new branch...')
const date = new Date()
const isoDate = date.toISOString().split('T')[0]
const timestamp = date.getTime()
const branchName = `update-electron-types-${isoDate}-${timestamp}`
execSync(`git checkout -b ${branchName} -t origin/main`, { stdio: 'inherit' })
// Update npm package to latest version
console.log(`Updating ${description}...`)
execSync(`npm install ${packageName}@latest`, {
stdio: 'inherit'
})
// Get the new version from package.json
const packageLock = JSON.parse(readFileSync('./package-lock.json', 'utf8'))
const newVersion = packageLock.packages[`node_modules/${packageName}`].version
// Stage changes
const message = `[chore] Update electron-types to ${newVersion}`
execSync('git add package.json package-lock.json', { stdio: 'inherit' })
execSync(`git commit -m "${message}"`, { stdio: 'inherit' })
// Create the PR
console.log('Creating PR...')
execSync(
`gh pr create --title "${message}" --label "dependencies" --body "Automated update of ${description} to version ${newVersion}."`,
{ stdio: 'inherit' }
)
console.log(
`✅ Successfully created PR for ${description} update to ${newVersion}`
)
} catch (error) {
console.error('❌ Error during update process:', error.message)
}

View File

@@ -18,7 +18,7 @@ import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
import config from '@/config'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { electronAPI, isElectron } from './utils/envUtil'
import { isElectron, showNativeMenu } from './utils/envUtil'
const workspaceStore = useWorkspaceStore()
const isLoading = computed<boolean>(() => workspaceStore.spinner)
@@ -34,7 +34,7 @@ const showContextMenu = (event: PointerEvent) => {
case target instanceof HTMLTextAreaElement:
case target instanceof HTMLInputElement && target.type === 'text':
// TODO: Context input menu explicitly for text input
electronAPI()?.showContextMenu({ type: 'text' })
showNativeMenu({ type: 'text' })
return
}
}

View File

@@ -0,0 +1 @@
{"last_node_id":16,"last_link_id":10,"nodes":[{"id":16,"type":"MarkdownNote","pos":[630,60],"size":[930,945],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[],"title":"Changelog","properties":{},"widgets_values":["# v0.3.11 (Pre-release)\n\n## What's Changed\n\n* Nvidia Cosmos 7B and 14B: text to video and image to video diffusion model support.\n <iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/watch?v=-OCwHBur0FM\" frameborder=\"0\" allowfullscreen></iframe>\n\n\n* New sampler: res_multistep\n* ckpt/pt/etc.. files are now always loaded safely on pytorch 2.4 and above.\n* Fix some cases of ancestral samplers not being deterministic.\n* Support ascend npu by @ji-huazhong [#5436](https://github.com/comfyanonymous/ComfyUI/pull/5436)\n* Add option to log non-error output to stdout by @webfiltered [#6243](https://github.com/comfyanonymous/ComfyUI/pull/6243)\n* serve workflow templates from custom_nodes by @bezo97 [#6193](https://github.com/comfyanonymous/ComfyUI/pull/6193)\n* Remove duplicate calls to INPUT_TYPES by @catboxanon [#6249](https://github.com/comfyanonymous/ComfyUI/pull/6249)\n* Fix Hook Keyframe 'guarantee_steps' behavior and add 'sigmas' by @Kosinkadink [#6273](https://github.com/comfyanonymous/ComfyUI/pull/6273)\n* Add kl_optimal scheduler by @blepping [#6206](https://github.com/comfyanonymous/ComfyUI/pull/6206)\n* (fix): \"verbose\" argument by @bigcat88 [#6289](https://github.com/comfyanonymous/ComfyUI/pull/6289)\n* Fix custom node type-hinting examples by @webfiltered [#6281](https://github.com/comfyanonymous/ComfyUI/pull/6281)\n* Add missing model_options param to finalize_default_conds call by @Kosinkadink [#6296](https://github.com/comfyanonymous/ComfyUI/pull/6296)\n* Fix unknown scheduler error handling in calculate_sigmas function by @blepping [#6280](https://github.com/comfyanonymous/ComfyUI/pull/6280)\n* Fix temporal tiling for Tiled VAE decoder, remove redundant tiles. by @kvochko [#6306](https://github.com/comfyanonymous/ComfyUI/pull/6306)\n* Update web content to release v1.6.14 by @huchenlei [#6312](https://github.com/comfyanonymous/ComfyUI/pull/6312)\n* add fov and mask for load 3d node by @jtydhr88 [#6308](https://github.com/comfyanonymous/ComfyUI/pull/6308)\n* Update web content to release v1.6.15 by @huchenlei [#6324](https://github.com/comfyanonymous/ComfyUI/pull/6324)\n* Update web content to release v1.6.16 by @huchenlei [#6335](https://github.com/comfyanonymous/ComfyUI/pull/6335)\n* Update web content to release v1.6.17 by @huchenlei [#6337](https://github.com/comfyanonymous/ComfyUI/pull/6337)\n* Add update-frontend github action by @huchenlei [#6336](https://github.com/comfyanonymous/ComfyUI/pull/6336)\n* Update CODEOWNERS by @yoland68 [#6338](https://github.com/comfyanonymous/ComfyUI/pull/6338)\n* In inner_sample, change \"sigmas\" to \"sample_sigmas\" by @Kosinkadink [#6360](https://github.com/comfyanonymous/ComfyUI/pull/6360)\n* Frontend Update: v1.6.18 by @huchenlei [#6368](https://github.com/comfyanonymous/ComfyUI/pull/6368)\n* Document get_attr and get_model_object by @huchenlei [#6357](https://github.com/comfyanonymous/ComfyUI/pull/6357)\n* fixed: robust loading \\`comfy.settings.json\\` by @ltdrdata [#6383](https://github.com/comfyanonymous/ComfyUI/pull/6383)\n* Add pyproject.toml by @huchenlei [#6386](https://github.com/comfyanonymous/ComfyUI/pull/6386)\n* Hooks Part 2 - TransformerOptionsHook and AdditionalModelsHook by @Kosinkadink [#6377](https://github.com/comfyanonymous/ComfyUI/pull/6377)\n* Merge ruff.toml into pyproject.toml by @huchenlei [#6431](https://github.com/comfyanonymous/ComfyUI/pull/6431)\n* (fix): load_extra_path_config: relative path not converted to a full path by @bigcat88 [#6395](https://github.com/comfyanonymous/ComfyUI/pull/6395)\n* Rewrite res_multistep sampler and implement res_multistep_cfg_pp sampler by @pamparamm [#6462](https://github.com/comfyanonymous/ComfyUI/pull/6462)\n* Add SetFirstSigma node by @catboxanon [#6459](https://github.com/comfyanonymous/ComfyUI/pull/6459)\n\n**Full Changelog**: [\\`v0.3.10...v0.3.11\\`](https://github.com/comfyanonymous/ComfyUI/compare/v0.3.10...v0.3.11)\n\n----\n\n> To disable showing changelog on new releases, go the the Changelog section in settings\n"],"color":"#222","bgcolor":"#000"}],"links":[],"groups":[],"config":{},"extra":{"ds":{"scale":1.503752370924104,"offset":[-489.9627968865069,158.59081129748483]}},"version":0.4}

View File

@@ -765,30 +765,6 @@ audio.comfy-audio.empty-audio-widget {
padding: var(--comfy-tree-explorer-item-padding) !important;
}
/* Load3d styles */
.comfy-load-3d,
.comfy-load-3d-animation,
.comfy-preview-3d,
.comfy-preview-3d-animation{
display: flex;
flex-direction: column;
background: transparent;
flex: 1;
position: relative;
overflow: hidden;
}
.comfy-load-3d canvas,
.comfy-load-3d-animation canvas,
.comfy-preview-3d canvas,
.comfy-preview-3d-animation canvas{
display: flex;
width: 100% !important;
height: 100% !important;
}
/* End of Load3d styles */
/* [Desktop] Electron window specific styles */
.app-drag {
app-region: drag;

View File

@@ -22,7 +22,7 @@
</template>
<template #item="{ item }">
<Button
:label="String(item.label)"
:label="item.label"
:icon="item.icon"
:severity="item.key === queueMode ? 'primary' : 'secondary'"
size="small"

View File

@@ -0,0 +1,40 @@
<template>
<div :class="['flex flex-wrap', $attrs.class]">
<div
v-for="checkbox in checkboxes"
:key="checkbox.value"
class="flex items-center gap-2"
>
<Checkbox
v-model="internalSelection"
:inputId="checkbox.value"
:value="checkbox.value"
/>
<label :for="checkbox.value" class="ml-2">{{ checkbox.label }}</label>
</div>
</div>
</template>
<script setup lang="ts">
import Checkbox from 'primevue/checkbox'
import { computed } from 'vue'
interface CheckboxItem {
label: string
value: string
}
const props = defineProps<{
checkboxes: CheckboxItem[]
modelValue: string[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string[]): void
}>()
const internalSelection = computed({
get: () => props.modelValue,
set: (value: string[]) => emit('update:modelValue', value)
})
</script>

View File

@@ -42,7 +42,7 @@
v-else
class="pi pi-palette"
:style="{ fontSize: '1.2rem' }"
v-tooltip="$t('color.custom')"
v-tooltip="$t('g.customColor')"
></i>
</template>
</SelectButton>

View File

@@ -18,7 +18,7 @@
:label="$t('g.download') + ' (' + fileSize + ')'"
size="small"
outlined
:disabled="!!props.error"
:disabled="props.error"
@click="triggerDownload"
v-if="status === null || status === 'error'"
icon="pi pi-download"
@@ -30,7 +30,7 @@
v-if="status === 'in_progress' || status === 'paused'"
>
<!-- Temporary fix for issue when % only comes into view only if the progress bar is large enough
https://comfy-organization.slack.com/archives/C07H3GLKDPF/p1731551013385499
https://comfy-organization.slack.com/archives/C07H3GLKDPF/p1731551013385499
-->
<ProgressBar
class="flex-1"
@@ -42,7 +42,7 @@
class="file-action-button"
size="small"
outlined
:disabled="!!props.error"
:disabled="props.error"
@click="triggerPauseDownload"
v-if="status === 'in_progress'"
icon="pi pi-pause-circle"
@@ -53,7 +53,7 @@
class="file-action-button"
size="small"
outlined
:disabled="!!props.error"
:disabled="props.error"
@click="triggerResumeDownload"
v-if="status === 'paused'"
icon="pi pi-play-circle"
@@ -64,7 +64,7 @@
class="file-action-button"
size="small"
outlined
:disabled="!!props.error"
:disabled="props.error"
@click="triggerCancelDownload"
icon="pi pi-times-circle"
severity="danger"

View File

@@ -15,7 +15,7 @@
:label="$t('g.download') + ' (' + fileSize + ')'"
size="small"
outlined
:disabled="!!props.error"
:disabled="props.error"
:title="props.url"
@click="download.triggerBrowserDownload"
/>

View File

@@ -1,17 +1,35 @@
<template>
<div class="color-picker-wrapper flex items-center gap-2">
<ColorPicker v-model="modelValue" />
<ColorPicker v-model="modelValue">
<template #header>
<div class="flex items-center justify-between p-2">
<span>{{ props.label }}</span>
<Button
v-if="props.defaultValue"
icon="pi pi-refresh"
text
size="small"
@click="resetColor"
/>
</div>
</template>
</ColorPicker>
<InputText v-model="modelValue" class="w-28" :placeholder="label" />
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import ColorPicker from 'primevue/colorpicker'
import InputText from 'primevue/inputtext'
const modelValue = defineModel<string>('modelValue')
defineProps<{
const props = defineProps<{
defaultValue?: string
label?: string
}>()
const resetColor = () => {
modelValue.value = props.defaultValue || '#000000'
}
</script>

View File

@@ -35,7 +35,6 @@ import CustomFormValue from '@/components/common/CustomFormValue.vue'
import FormColorPicker from '@/components/common/FormColorPicker.vue'
import FormImageUpload from '@/components/common/FormImageUpload.vue'
import InputSlider from '@/components/common/InputSlider.vue'
import UrlInput from '@/components/common/UrlInput.vue'
import { FormItem } from '@/types/settingTypes'
const formValue = defineModel<any>('formValue')
@@ -92,8 +91,6 @@ function getFormComponent(item: FormItem): Component {
return FormImageUpload
case 'color':
return FormColorPicker
case 'url':
return UrlInput
default:
return InputText
}

View File

@@ -0,0 +1,66 @@
<!-- A simple read-only terminal component that displays logs. -->
<template>
<div class="p-terminal rounded-none h-full w-full">
<ScrollPanel class="h-full w-full" ref="scrollPanelRef">
<pre class="px-4 whitespace-pre-wrap">{{ log }}</pre>
</ScrollPanel>
</div>
</template>
<script setup lang="ts">
import ScrollPanel from 'primevue/scrollpanel'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
const props = defineProps<{
fetchLogs: () => Promise<string>
fetchInterval: number
}>()
const log = ref<string>('')
const scrollPanelRef = ref<InstanceType<typeof ScrollPanel> | null>(null)
/**
* Whether the user has scrolled to the bottom of the terminal.
* This is used to prevent the terminal from scrolling to the bottom
* when new logs are fetched.
*/
const scrolledToBottom = ref(false)
let intervalId: number = 0
onMounted(async () => {
const element = scrollPanelRef.value?.$el
const scrollContainer = element?.querySelector('.p-scrollpanel-content')
if (scrollContainer) {
scrollContainer.addEventListener('scroll', () => {
scrolledToBottom.value =
scrollContainer.scrollTop + scrollContainer.clientHeight ===
scrollContainer.scrollHeight
})
}
const scrollToBottom = () => {
if (scrollContainer) {
scrollContainer.scrollTop = scrollContainer.scrollHeight
}
}
watch(log, () => {
if (scrolledToBottom.value) {
scrollToBottom()
}
})
const fetchLogs = async () => {
log.value = await props.fetchLogs()
}
await fetchLogs()
scrollToBottom()
intervalId = window.setInterval(fetchLogs, props.fetchInterval)
})
onBeforeUnmount(() => {
window.clearInterval(intervalId)
})
</script>

View File

@@ -1,53 +0,0 @@
<!--
A refresh button that disables and shows a progress spinner whilst active.
Usage:
```vue
<RefreshButton
v-model="isRefreshing"
:outlined="false"
@refresh="refresh"
/>
```
-->
<template>
<Button
class="relative p-button-icon-only"
:outlined="props.outlined"
:severity="props.severity"
:disabled="active || props.disabled"
@click="(event) => $emit('refresh', event)"
>
<span
class="p-button-icon pi pi-refresh transition-all"
:class="{ 'opacity-0': active }"
data-pc-section="icon"
></span>
<span class="p-button-label" data-pc-section="label">&nbsp;</span>
<ProgressSpinner v-show="active" class="absolute w-1/2 h-1/2" />
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import ProgressSpinner from 'primevue/progressspinner'
import { VueSeverity } from '@/types/primeVueTypes'
// Properties
interface Props {
outlined?: boolean
disabled?: boolean
severity?: VueSeverity
}
const props = withDefaults(defineProps<Props>(), {
outlined: true,
severity: 'secondary'
})
// Model
const active = defineModel<boolean>({ required: true })
// Emits
defineEmits(['refresh'])
</script>

View File

@@ -1,10 +1,10 @@
<template>
<div>
<div :class="props.class">
<IconField>
<Button
v-if="filterIcon"
v-if="props.filterIcon"
class="p-inputicon filter-button"
:icon="filterIcon"
:icon="props.filterIcon"
text
severity="contrast"
@click="$emit('showFilter', $event)"
@@ -12,12 +12,12 @@
<InputText
class="search-box-input w-full"
@input="handleInput"
:modelValue="modelValue"
:placeholder="placeholder"
:modelValue="props.modelValue"
:placeholder="props.placeholder"
/>
<InputIcon v-if="!modelValue" :class="icon" />
<InputIcon v-if="!props.modelValue" :class="props.icon" />
<Button
v-if="modelValue"
v-if="props.modelValue"
class="p-inputicon clear-button"
icon="pi pi-times"
text
@@ -47,36 +47,40 @@ import Button from 'primevue/button'
import IconField from 'primevue/iconfield'
import InputIcon from 'primevue/inputicon'
import InputText from 'primevue/inputtext'
import { toRefs } from 'vue'
import type { SearchFilter } from './SearchFilterChip.vue'
import SearchFilterChip from './SearchFilterChip.vue'
const {
modelValue,
placeholder = 'Search...',
icon = 'pi pi-search',
debounceTime = 300,
filterIcon,
filters = []
} = defineProps<{
modelValue: string
placeholder?: string
icon?: string
debounceTime?: number
filterIcon?: string
filters?: TFilter[]
}>()
const props = withDefaults(
defineProps<{
class?: string
modelValue: string
placeholder?: string
icon?: string
debounceTime?: number
filterIcon?: string
filters?: TFilter[]
}>(),
{
placeholder: 'Search...',
icon: 'pi pi-search',
debounceTime: 300
}
)
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
(e: 'search', value: string, filters: TFilter[]): void
(e: 'showFilter', event: Event): void
(e: 'removeFilter', filter: TFilter): void
}>()
const { filters } = toRefs(props)
const emit = defineEmits([
'update:modelValue',
'search',
'showFilter',
'removeFilter'
])
const emitSearch = debounce((value: string) => {
emit('search', value, filters)
}, debounceTime)
emit('search', value, props.filters)
}, props.debounceTime)
const handleInput = (event: Event) => {
const target = event.target as HTMLInputElement

View File

@@ -19,7 +19,6 @@
v-for="device in props.stats.devices"
:key="device.index"
:header="device.name"
:value="device.index"
>
<DeviceInfo :device="device" />
</TabPanel>

View File

@@ -8,11 +8,9 @@
selectionMode="single"
:pt="{
nodeLabel: 'tree-explorer-node-label',
nodeContent: ({ context }) => ({
onClick: (e: MouseEvent) =>
onNodeContentClick(e, context.node as RenderedTreeExplorerNode),
onContextmenu: (e: MouseEvent) =>
handleContextMenu(e, context.node as RenderedTreeExplorerNode)
nodeContent: ({ props }) => ({
onClick: (e: MouseEvent) => onNodeContentClick(e, props.node),
onContextmenu: (e: MouseEvent) => handleContextMenu(props.node, e)
}),
nodeToggleButton: () => ({
onClick: (e: MouseEvent) => {
@@ -154,7 +152,7 @@ const menuItems = computed<MenuItem[]>(() =>
}))
)
const handleContextMenu = (e: MouseEvent, node: RenderedTreeExplorerNode) => {
const handleContextMenu = (node: RenderedTreeExplorerNode, e: MouseEvent) => {
menuTargetNode.value = node
emit('contextMenu', node, e)
if (menuItems.value.filter((item) => item.visible).length > 0) {

View File

@@ -1,116 +0,0 @@
<template>
<IconField class="w-full">
<InputText
v-bind="$attrs"
:model-value="internalValue"
class="w-full"
:invalid="validationState === UrlValidationState.INVALID"
@update:model-value="handleInput"
@blur="handleBlur"
/>
<InputIcon
:class="{
'pi pi-spin pi-spinner text-neutral-400':
validationState === UrlValidationState.LOADING,
'pi pi-check text-green-500 cursor-pointer':
validationState === UrlValidationState.VALID,
'pi pi-times text-red-500 cursor-pointer':
validationState === UrlValidationState.INVALID
}"
@click="validateUrl(props.modelValue)"
/>
</IconField>
</template>
<script setup lang="ts">
import IconField from 'primevue/iconfield'
import InputIcon from 'primevue/inputicon'
import InputText from 'primevue/inputtext'
import { onMounted, ref, watch } from 'vue'
import { isValidUrl } from '@/utils/formatUtil'
import { checkUrlReachable } from '@/utils/networkUtil'
const props = defineProps<{
modelValue: string
validateUrlFn?: (url: string) => Promise<boolean>
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
enum UrlValidationState {
IDLE = 'IDLE',
LOADING = 'LOADING',
VALID = 'VALID',
INVALID = 'INVALID'
}
const validationState = ref<UrlValidationState>(UrlValidationState.IDLE)
// Add internal value state
const internalValue = ref(props.modelValue)
// Watch for external modelValue changes
watch(
() => props.modelValue,
async (newValue: string) => {
internalValue.value = newValue
await validateUrl(newValue)
}
)
// Validate on mount
onMounted(async () => {
await validateUrl(props.modelValue)
})
const handleInput = (value: string) => {
// Update internal value without emitting
internalValue.value = value
// Reset validation state when user types
validationState.value = UrlValidationState.IDLE
}
const handleBlur = async () => {
// Emit the update only on blur
emit('update:modelValue', internalValue.value)
}
// Default validation implementation
const defaultValidateUrl = async (url: string): Promise<boolean> => {
if (!isValidUrl(url)) return false
try {
return await checkUrlReachable(url)
} catch {
return false
}
}
const validateUrl = async (value: string) => {
if (validationState.value === UrlValidationState.LOADING) return
const url = value.trim()
// Reset state
validationState.value = UrlValidationState.IDLE
// Skip validation if empty
if (!url) return
validationState.value = UrlValidationState.LOADING
try {
const isValid = await (props.validateUrlFn ?? defaultValidateUrl)(url)
validationState.value = isValid
? UrlValidationState.VALID
: UrlValidationState.INVALID
} catch {
validationState.value = UrlValidationState.INVALID
}
}
// Add inheritAttrs option to prevent attrs from being applied to root element
defineOptions({
inheritAttrs: false
})
</script>

View File

@@ -1,158 +0,0 @@
import { mount } from '@vue/test-utils'
import PrimeVue from 'primevue/config'
import IconField from 'primevue/iconfield'
import InputIcon from 'primevue/inputicon'
import InputText from 'primevue/inputtext'
import { beforeEach, describe, expect, it } from 'vitest'
import { createApp, nextTick } from 'vue'
import UrlInput from '../UrlInput.vue'
describe('UrlInput', () => {
beforeEach(() => {
const app = createApp({})
app.use(PrimeVue)
})
const mountComponent = (props: any, options = {}) => {
return mount(UrlInput, {
global: {
plugins: [PrimeVue],
components: { IconField, InputIcon, InputText }
},
props,
...options
})
}
it('passes through additional attributes to input element', () => {
const wrapper = mountComponent({
modelValue: '',
placeholder: 'Enter URL',
disabled: true
})
expect(wrapper.find('input').attributes('disabled')).toBe('')
})
it('emits update:modelValue on blur', async () => {
const wrapper = mountComponent({
modelValue: '',
placeholder: 'Enter URL'
})
const input = wrapper.find('input')
await input.setValue('https://test.com')
await input.trigger('blur')
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([
'https://test.com'
])
})
it('renders spinner when validation is loading', async () => {
const wrapper = mountComponent({
modelValue: '',
placeholder: 'Enter URL',
validateUrlFn: () =>
new Promise(() => {
// Never resolves, simulating perpetual loading state
})
})
wrapper.setProps({ modelValue: 'https://test.com' })
await nextTick()
await nextTick()
expect(wrapper.find('.pi-spinner').exists()).toBe(true)
})
it('renders check icon when validation is valid', async () => {
const wrapper = mountComponent({
modelValue: '',
placeholder: 'Enter URL',
validateUrlFn: () => Promise.resolve(true)
})
wrapper.setProps({ modelValue: 'https://test.com' })
await nextTick()
await nextTick()
expect(wrapper.find('.pi-check').exists()).toBe(true)
})
it('renders cross icon when validation is invalid', async () => {
const wrapper = mountComponent({
modelValue: '',
placeholder: 'Enter URL',
validateUrlFn: () => Promise.resolve(false)
})
wrapper.setProps({ modelValue: 'https://test.com' })
await nextTick()
await nextTick()
expect(wrapper.find('.pi-times').exists()).toBe(true)
})
it('validates on mount', async () => {
const wrapper = mountComponent({
modelValue: 'https://test.com',
validateUrlFn: () => Promise.resolve(true)
})
await nextTick()
await nextTick()
expect(wrapper.find('.pi-check').exists()).toBe(true)
})
it('triggers validation when clicking the validation icon', async () => {
let validationCount = 0
const wrapper = mountComponent({
modelValue: 'https://test.com',
validateUrlFn: () => {
validationCount++
return Promise.resolve(true)
}
})
// Wait for initial validation
await nextTick()
await nextTick()
// Click the validation icon
await wrapper.find('.pi-check').trigger('click')
await nextTick()
await nextTick()
expect(validationCount).toBe(2) // Once on mount, once on click
})
it('prevents multiple simultaneous validations', async () => {
let validationCount = 0
const wrapper = mountComponent({
modelValue: '',
validateUrlFn: () => {
validationCount++
return new Promise(() => {
// Never resolves, simulating perpetual loading state
})
}
})
wrapper.setProps({ modelValue: 'https://test.com' })
await nextTick()
await nextTick()
// Trigger multiple validations in quick succession
wrapper.find('.pi-spinner').trigger('click')
wrapper.find('.pi-spinner').trigger('click')
wrapper.find('.pi-spinner').trigger('click')
await nextTick()
await nextTick()
expect(validationCount).toBe(1) // Only the initial validation should occur
})
})

View File

@@ -28,7 +28,6 @@
</template>
<ReportIssuePanel
v-if="sendReportOpen"
:title="$t('issueReport.submitErrorReport')"
error-type="graphExecutionError"
:extra-fields="[stackTraceField]"
:tags="{ exceptionMessage: props.error.exception_message }"
@@ -90,10 +89,10 @@ const stackTraceField = computed<ReportField>(() => {
label: t('issueReport.stackTrace'),
value: 'StackTrace',
optIn: true,
getData: () => ({
data: {
nodeType: props.error.node_type,
stackTrace: props.error.traceback?.join('\n')
})
}
}
})

View File

@@ -1,31 +0,0 @@
<template>
<div class="p-2 h-full" aria-labelledby="issue-report-title">
<Panel
:pt="{
root: 'border-none',
content: 'p-0'
}"
>
<template #header>
<header class="flex flex-col items-center w-full">
<h2 id="issue-report-title" class="text-4xl">{{ title }}</h2>
<span v-if="subtitle" class="text-muted mt-0">{{ subtitle }}</span>
</header>
</template>
<ReportIssuePanel v-bind="panelProps" :pt="{ root: 'border-none' }" />
</Panel>
</div>
</template>
<script setup lang="ts">
import Panel from 'primevue/panel'
import ReportIssuePanel from '@/components/dialog/content/error/ReportIssuePanel.vue'
import type { IssueReportPanelProps } from '@/types/issueReportTypes'
defineProps<{
title: string
subtitle?: string
panelProps: IssueReportPanelProps
}>()
</script>

View File

@@ -2,15 +2,9 @@
<NoResultsPlaceholder
class="pb-0"
icon="pi pi-exclamation-circle"
:title="t('missingModelsDialog.missingModels')"
:message="t('missingModelsDialog.missingModelsMessage')"
title="Missing Models"
message="When loading the graph, the following models were not found"
/>
<div class="flex gap-1 mb-4">
<Checkbox v-model="doNotAskAgain" binary input-id="doNotAskAgain" />
<label for="doNotAskAgain">{{
t('missingModelsDialog.doNotAskAgain')
}}</label>
</div>
<ListBox :options="missingModels" class="comfy-missing-models">
<template #option="{ option }">
<Suspense v-if="isElectron()">
@@ -31,14 +25,11 @@
</template>
<script setup lang="ts">
import Checkbox from 'primevue/checkbox'
import ListBox from 'primevue/listbox'
import { computed, onBeforeUnmount, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { computed, ref } from 'vue'
import FileDownload from '@/components/common/FileDownload.vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import { useSettingStore } from '@/stores/settingStore'
import { isElectron } from '@/utils/envUtil'
// TODO: Read this from server internal API rather than hardcoding here
@@ -67,10 +58,6 @@ const props = defineProps<{
paths: Record<string, string[]>
}>()
const { t } = useI18n()
const doNotAskAgain = ref(false)
const modelDownloads = ref<Record<string, ModelInfo>>({})
const missingModels = computed(() => {
return props.missingModels.map((model) => {
@@ -120,12 +107,6 @@ const missingModels = computed(() => {
}
})
})
onBeforeUnmount(() => {
if (doNotAskAgain.value) {
useSettingStore().set('Comfy.Workflow.ShowMissingModelsWarning', false)
}
})
</script>
<style scoped>

View File

@@ -1,252 +1,201 @@
<template>
<Form
v-slot="$form"
@submit="submit"
:resolver="zodResolver(issueReportSchema)"
>
<Panel :pt="$attrs.pt">
<template #header>
<div class="flex items-center gap-2">
<span class="font-bold">{{ title }}</span>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-4">
<Button
v-tooltip="!submitted ? $t('g.reportIssueTooltip') : undefined"
:label="submitted ? $t('g.reportSent') : $t('g.reportIssue')"
:severity="submitted ? 'secondary' : 'primary'"
:icon="submitted ? 'pi pi-check' : 'pi pi-send'"
:disabled="submitted"
type="submit"
/>
</div>
</template>
<div class="p-4 mt-2 border border-round surface-border shadow-1">
<div class="flex flex-row gap-3 mb-2">
<div v-for="field in fields" :key="field.value">
<FormField
v-if="field.optIn"
v-slot="$field"
:name="field.value"
class="flex space-x-1"
>
<Checkbox
v-bind="$field"
:inputId="field.value"
:value="field.value"
v-model="selection"
/>
<label :for="field.value">{{ field.label }}</label>
</FormField>
</div>
</div>
<FormField class="mb-4" v-slot="$field" name="details">
<Textarea
v-bind="$field"
class="w-full"
rows="5"
:placeholder="$t('issueReport.provideAdditionalDetails')"
:aria-label="$t('issueReport.provideAdditionalDetails')"
/>
<Message
v-if="$field?.error && $field.touched && $field.value"
severity="error"
size="small"
variant="simple"
>
{{ t('issueReport.validation.maxLength') }}
</Message>
</FormField>
<FormField v-slot="$field" name="contactInfo">
<InputText
v-bind="$field"
class="w-full"
:placeholder="$t('issueReport.provideEmail')"
/>
<Message
v-if="$field?.error && $field.touched && $field.value !== ''"
severity="error"
size="small"
variant="simple"
>
{{ t('issueReport.validation.invalidEmail') }}
</Message>
</FormField>
<div class="flex flex-row gap-3 mt-2">
<div v-for="checkbox in contactCheckboxes" :key="checkbox.value">
<FormField
v-slot="$field"
:name="checkbox.value"
class="flex space-x-1"
>
<Checkbox
v-bind="$field"
:inputId="checkbox.value"
:value="checkbox.value"
v-model="contactPrefs"
:disabled="
$form.contactInfo?.error || !$form.contactInfo?.value
"
/>
<label :for="checkbox.value">{{ checkbox.label }}</label>
</FormField>
</div>
</div>
<Panel>
<template #header>
<div class="flex items-center gap-2">
<span class="font-bold">{{ $t('issueReport.submitErrorReport') }}</span>
</div>
</Panel>
</Form>
</template>
<template #footer>
<div class="flex justify-end">
<Button
v-tooltip="$t('g.reportIssueTooltip')"
:label="submitted ? $t('g.reportSent') : $t('g.reportIssue')"
:severity="isButtonDisabled ? 'secondary' : 'primary'"
:icon="icon"
:disabled="isButtonDisabled"
@click="reportIssue"
/>
</div>
</template>
<div class="p-4 mt-4 border border-round surface-border shadow-1">
<CheckboxGroup
v-model="selection"
class="gap-4 mb-4"
:checkboxes="reportCheckboxes"
/>
<div class="mb-4">
<InputText
v-model="contactInfo"
class="w-full"
:placeholder="$t('issueReport.provideEmail')"
:maxlength="CONTACT_MAX_LEN"
/>
<CheckboxGroup
v-model="contactPrefs"
class="gap-3 mt-2"
:checkboxes="contactCheckboxes"
/>
</div>
<div class="mb-4">
<Textarea
v-model="details"
class="w-full"
rows="4"
:maxlength="DETAILS_MAX_LEN"
:placeholder="$t('issueReport.provideAdditionalDetails')"
:aria-label="$t('issueReport.provideAdditionalDetails')"
/>
</div>
</div>
</Panel>
</template>
<script setup lang="ts">
import { Form, FormField, type FormSubmitEvent } from '@primevue/forms'
// @ts-expect-error https://github.com/primefaces/primevue/issues/6722
import { zodResolver } from '@primevue/forms/resolvers/zod'
import type { CaptureContext, User } from '@sentry/core'
import { captureMessage } from '@sentry/core'
import _ from 'lodash'
import cloneDeep from 'lodash/cloneDeep'
import Button from 'primevue/button'
import Checkbox from 'primevue/checkbox'
import InputText from 'primevue/inputtext'
import Message from 'primevue/message'
import Panel from 'primevue/panel'
import Textarea from 'primevue/textarea'
import { useToast } from 'primevue/usetoast'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import CheckboxGroup from '@/components/common/CheckboxGroup.vue'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import {
type IssueReportFormData,
type ReportField,
issueReportSchema
} from '@/types/issueReportTypes'
import type {
DefaultField,
IssueReportPanelProps
} from '@/types/issueReportTypes'
import { isElectron } from '@/utils/envUtil'
import type { DefaultField, ReportField } from '@/types/issueReportTypes'
const ISSUE_NAME = 'User reported issue'
const DETAILS_MAX_LEN = 5_000
const CONTACT_MAX_LEN = 320
const props = defineProps<IssueReportPanelProps>()
const { defaultFields = ['Workflow', 'Logs', 'SystemStats', 'Settings'] } =
props
const props = defineProps<{
errorType: string
defaultFields?: DefaultField[]
extraFields?: ReportField[]
tags?: Record<string, string>
}>()
const {
defaultFields = ['Workflow', 'Logs', 'SystemStats', 'Settings'],
tags = {}
} = props
const { t } = useI18n()
const toast = useToast()
const selection = ref<string[]>([])
const contactPrefs = ref<string[]>([])
const contactInfo = ref('')
const details = ref('')
const submitting = ref(false)
const submitted = ref(false)
const followUp = computed(() => contactPrefs.value.includes('FollowUp'))
const notifyResolve = computed(() => contactPrefs.value.includes('Resolution'))
const icon = computed(() => {
if (submitting.value) return 'pi pi-spin pi-spinner'
if (submitted.value) return 'pi pi-check'
return 'pi pi-send'
})
const isFormEmpty = computed(() => !selection.value.length && !details.value)
const isButtonDisabled = computed(
() => submitted.value || submitting.value || isFormEmpty.value
)
const contactCheckboxes = [
{ label: t('issueReport.contactFollowUp'), value: 'followUp' },
{ label: t('issueReport.notifyResolve'), value: 'notifyOnResolution' }
{ label: t('issueReport.contactFollowUp'), value: 'FollowUp' },
{ label: t('issueReport.notifyResolve'), value: 'Resolution' }
]
const defaultFieldsConfig: ReportField[] = [
{
label: t('issueReport.systemStats'),
value: 'SystemStats',
getData: () => api.getSystemStats(),
optIn: true
},
{
label: t('g.workflow'),
value: 'Workflow',
getData: () => cloneDeep(app.graph.asSerialisable()),
optIn: true
},
{
label: t('g.logs'),
value: 'Logs',
getData: () => api.getLogs(),
optIn: true
},
{
label: t('g.settings'),
value: 'Settings',
getData: () => api.getSettings(),
optIn: true
}
const defaultReportCheckboxes = [
{ label: t('g.workflow'), value: 'Workflow' },
{ label: t('g.logs'), value: 'Logs' },
{ label: t('issueReport.systemStats'), value: 'SystemStats' },
{ label: t('g.settings'), value: 'Settings' }
]
const fields = computed(() => [
...defaultFieldsConfig.filter(({ value }) =>
const reportCheckboxes = computed(() => [
...(props.extraFields?.map(({ label, value }) => ({ label, value })) ?? []),
...defaultReportCheckboxes.filter(({ value }) =>
defaultFields.includes(value as DefaultField)
),
...(props.extraFields ?? [])
)
])
const createUser = (formData: IssueReportFormData): User => ({
email: formData.contactInfo || undefined
})
const getUserInfo = (): User => ({ email: contactInfo.value })
const createExtraData = async (formData: IssueReportFormData) => {
const result = {}
const isChecked = (fieldValue: string) => formData[fieldValue]
const getLogs = async () =>
selection.value.includes('Logs') ? api.getLogs() : null
await Promise.all(
fields.value
.filter((field) => !field.optIn || isChecked(field.value))
.map(async (field) => {
try {
result[field.value] = await field.getData()
} catch (error) {
console.error(`Failed to collect ${field.value}:`, error)
result[field.value] = { error: String(error) }
}
})
)
const getSystemStats = async () =>
selection.value.includes('SystemStats') ? api.getSystemStats() : null
return result
const getSettings = async () =>
selection.value.includes('Settings') ? api.getSettings() : null
const getWorkflow = () =>
selection.value.includes('Workflow')
? cloneDeep(app.graph.asSerialisable())
: null
const createDefaultFields = async () => {
const [settings, systemStats, logs, workflow] = await Promise.all([
getSettings(),
getSystemStats(),
getLogs(),
getWorkflow()
])
return { settings, systemStats, logs, workflow }
}
const createCaptureContext = async (
formData: IssueReportFormData
): Promise<CaptureContext> => {
const createExtraFields = (): Record<string, unknown> | undefined => {
if (!props.extraFields) return undefined
return props.extraFields
.filter((field) => !field.optIn || selection.value.includes(field.value))
.reduce((acc, field) => ({ ...acc, ...cloneDeep(field.data) }), {})
}
const createFeedback = () => {
return {
user: createUser(formData),
details: details.value,
contactPreferences: {
followUp: followUp.value,
notifyOnResolution: notifyResolve.value
}
}
}
const createCaptureContext = async (): Promise<CaptureContext> => {
return {
user: getUserInfo(),
level: 'error',
tags: {
errorType: props.errorType,
followUp: formData.contactInfo ? formData.followUp : false,
notifyOnResolution: formData.contactInfo
? formData.notifyOnResolution
: false,
isElectron: isElectron(),
..._.mapValues(props.tags, (tag) => _.trim(tag).replace(/[\n\r\t]/g, ' '))
...tags
},
extra: {
details: formData.details,
...(await createExtraData(formData))
...createFeedback(),
...(await createDefaultFields()),
...createExtraFields()
}
}
}
const submit = async (event: FormSubmitEvent) => {
if (event.valid) {
try {
const captureContext = await createCaptureContext(event.values)
captureMessage(ISSUE_NAME, captureContext)
submitted.value = true
toast.add({
severity: 'success',
summary: t('g.reportSent'),
life: 3000
})
} catch (error) {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: error.message,
life: 3000
})
}
const reportIssue = async () => {
if (isButtonDisabled.value) return
submitting.value = true
try {
captureMessage(ISSUE_NAME, await createCaptureContext())
submitted.value = true
toast.add({
severity: 'success',
summary: t('g.reportSent'),
life: 3000
})
} finally {
submitting.value = false
}
}
</script>

View File

@@ -1,51 +1,26 @@
// @ts-strict-ignore
import { Form } from '@primevue/forms'
import { createTestingPinia } from '@pinia/testing'
import { mount } from '@vue/test-utils'
import Checkbox from 'primevue/checkbox'
import Button from 'primevue/button'
import PrimeVue from 'primevue/config'
import InputText from 'primevue/inputtext'
import Panel from 'primevue/panel'
import Textarea from 'primevue/textarea'
import Tooltip from 'primevue/tooltip'
import { describe, expect, it, vi } from 'vitest'
import { beforeAll, describe, expect, it, vi } from 'vitest'
import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'
import CheckboxGroup from '@/components/common/CheckboxGroup.vue'
import enMesages from '@/locales/en/main.json'
import { IssueReportPanelProps } from '@/types/issueReportTypes'
import { DefaultField, ReportField } from '@/types/issueReportTypes'
import ReportIssuePanel from '../ReportIssuePanel.vue'
const DEFAULT_FIELDS = ['Workflow', 'Logs', 'Settings', 'SystemStats']
const CUSTOM_FIELDS = [
{
label: 'Custom Field',
value: 'CustomField',
optIn: true,
getData: () => 'mock data'
}
]
async function getSubmittedContext() {
const { captureMessage } = (await import('@sentry/core')) as any
return captureMessage.mock.calls[0][1]
}
async function submitForm(wrapper: any) {
await wrapper.findComponent(Form).trigger('submit')
return getSubmittedContext()
}
async function findAndUpdateCheckbox(
wrapper: any,
value: string,
checked = true
) {
const checkbox = wrapper
.findAllComponents(Checkbox)
.find((c: any) => c.props('value') === value)
if (!checkbox) throw new Error(`Checkbox with value "${value}" not found`)
await checkbox.vm.$emit('update:modelValue', checked)
return checkbox
type ReportIssuePanelProps = {
errorType: string
defaultFields?: DefaultField[]
extraFields?: ReportField[]
}
const i18n = createI18n({
@@ -82,64 +57,18 @@ vi.mock('@sentry/core', () => ({
captureMessage: vi.fn()
}))
vi.mock('@primevue/forms', () => ({
Form: {
name: 'Form',
template:
'<form @submit.prevent="onSubmit"><slot :values="formValues" /></form>',
props: ['resolver'],
data() {
return {
formValues: {}
}
},
methods: {
onSubmit() {
this.$emit('submit', {
valid: true,
values: this.formValues
})
},
updateFieldValue(name: string, value: any) {
this.formValues[name] = value
}
}
},
FormField: {
name: 'FormField',
template:
'<div><slot :modelValue="modelValue" @update:modelValue="updateValue" /></div>',
props: ['name'],
data() {
return {
modelValue: ''
}
},
methods: {
updateValue(value) {
this.modelValue = value
let parent = this.$parent
while (parent && parent.$options.name !== 'Form') {
parent = parent.$parent
}
if (parent) {
parent.updateFieldValue(this.name, value)
}
}
}
}
}))
describe('ReportIssuePanel', () => {
beforeEach(() => {
vi.clearAllMocks()
beforeAll(() => {
const app = createApp({})
app.use(PrimeVue)
})
const mountComponent = (props: IssueReportPanelProps, options = {}): any => {
const mountComponent = (props: ReportIssuePanelProps, options = {}): any => {
return mount(ReportIssuePanel, {
global: {
plugins: [PrimeVue, i18n],
directives: { tooltip: Tooltip }
plugins: [PrimeVue, createTestingPinia(), i18n],
directives: { tooltip: Tooltip },
components: { InputText, Button, Panel, Textarea, CheckboxGroup }
},
props,
...options
@@ -149,66 +78,44 @@ describe('ReportIssuePanel', () => {
it('renders the panel with all required components', () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
expect(wrapper.find('.p-panel').exists()).toBe(true)
expect(wrapper.findAllComponents(Checkbox).length).toBe(6)
expect(wrapper.findAllComponents(CheckboxGroup).length).toBe(2)
expect(wrapper.findComponent(InputText).exists()).toBe(true)
expect(wrapper.findComponent(Textarea).exists()).toBe(true)
expect(wrapper.findComponent(Button).exists()).toBe(true)
})
it('updates selection when checkboxes are selected', async () => {
const wrapper = mountComponent({
errorType: 'Test Error'
})
const checkboxes = wrapper.findAllComponents(Checkbox)
for (const field of DEFAULT_FIELDS) {
const checkbox = checkboxes.find(
(checkbox) => checkbox.props('value') === field
)
expect(checkbox).toBeDefined()
await checkbox?.vm.$emit('update:modelValue', [field])
expect(wrapper.vm.selection).toContain(field)
}
const wrapper = mountComponent({ errorType: 'Test Error' })
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
await checkboxes?.setValue(['Workflow', 'Logs'])
expect(wrapper.vm.selection).toEqual(['Workflow', 'Logs'])
})
it('updates contactInfo when input is changed', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const input = wrapper.findComponent(InputText)
await input.vm.$emit('update:modelValue', 'test@example.com')
const context = await submitForm(wrapper)
expect(context.user.email).toBe('test@example.com')
await input.setValue('test@example.com')
expect(wrapper.vm.contactInfo).toBe('test@example.com')
})
it('updates additional details when textarea is changed', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const textarea = wrapper.findComponent(Textarea)
await textarea.vm.$emit('update:modelValue', 'This is a test detail.')
const context = await submitForm(wrapper)
expect(context.extra.details).toBe('This is a test detail.')
await textarea.setValue('This is a test detail.')
expect(wrapper.vm.details).toBe('This is a test detail.')
})
it('set contact preferences back to false if email is removed', async () => {
it('updates contactPrefs when preferences are selected', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const input = wrapper.findComponent(InputText)
const preferences = wrapper.findAllComponents(CheckboxGroup).at(1)
await preferences?.setValue(['FollowUp'])
expect(wrapper.vm.contactPrefs).toEqual(['FollowUp'])
})
// Set a valid email, enabling the contact preferences to be changed
await input.vm.$emit('update:modelValue', 'name@example.com')
// Enable both contact preferences
for (const pref of ['followUp', 'notifyOnResolution']) {
await findAndUpdateCheckbox(wrapper, pref)
}
// Change the email back to empty
await input.vm.$emit('update:modelValue', '')
const context = await submitForm(wrapper)
// Check that the contact preferences are back to false automatically
expect(context.tags.followUp).toBe(false)
expect(context.tags.notifyOnResolution).toBe(false)
it('does not allow submission if the form is empty', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
await wrapper.vm.reportIssue()
expect(wrapper.vm.submitted).toBe(false)
})
it('renders with overridden default fields', () => {
@@ -216,87 +123,83 @@ describe('ReportIssuePanel', () => {
errorType: 'Test Error',
defaultFields: ['Settings']
})
// Filter out the contact preferences checkboxes
const fieldCheckboxes = wrapper
.findAllComponents(Checkbox)
.filter(
(checkbox) =>
!['followUp', 'notifyOnResolution'].includes(checkbox.props('value'))
)
expect(fieldCheckboxes.length).toBe(1)
expect(fieldCheckboxes.at(0)?.props('value')).toBe('Settings')
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
expect(checkboxes?.props('checkboxes')).toEqual([
{ label: 'Settings', value: 'Settings' }
])
})
it('renders additional fields when extraFields prop is provided', () => {
const wrapper = mountComponent({
errorType: 'Test Error',
extraFields: CUSTOM_FIELDS
const extraFields = [
{ label: 'Custom Field', value: 'CustomField', optIn: true, data: {} }
]
const wrapper = mountComponent({ errorType: 'Test Error', extraFields })
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
expect(checkboxes?.props('checkboxes')).toContainEqual({
label: 'Custom Field',
value: 'CustomField'
})
const customCheckbox = wrapper
.findAllComponents(Checkbox)
.find((checkbox) => checkbox.props('value') === 'CustomField')
expect(customCheckbox).toBeDefined()
})
it('allows custom fields to be selected', async () => {
const wrapper = mountComponent({
errorType: 'Test Error',
extraFields: CUSTOM_FIELDS
})
await findAndUpdateCheckbox(wrapper, 'CustomField')
const context = await submitForm(wrapper)
expect(context.extra.CustomField).toBe('mock data')
})
it('does not submit unchecked fields', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const textarea = wrapper.findComponent(Textarea)
// Set details but don't check any field checkboxes
await textarea.vm.$emit(
'update:modelValue',
'Report with only text but no fields selected'
)
const context = await submitForm(wrapper)
await textarea.setValue('Report with only text but no fields selected')
await wrapper.vm.reportIssue()
// Verify none of the optional fields were included
for (const field of DEFAULT_FIELDS) {
expect(context.extra[field]).toBeUndefined()
}
const { captureMessage } = (await import('@sentry/core')) as any
const captureContext = captureMessage.mock.calls[0][1]
expect(captureContext.extra.logs).toBeNull()
expect(captureContext.extra.systemStats).toBeNull()
expect(captureContext.extra.settings).toBeNull()
expect(captureContext.extra.workflow).toBeNull()
})
it.each([
{
checkbox: 'Logs',
apiMethod: 'getLogs',
expectedKey: 'Logs',
expectedKey: 'logs',
mockValue: 'mock logs'
},
{
checkbox: 'SystemStats',
apiMethod: 'getSystemStats',
expectedKey: 'SystemStats',
expectedKey: 'systemStats',
mockValue: 'mock stats'
},
{
checkbox: 'Settings',
apiMethod: 'getSettings',
expectedKey: 'Settings',
expectedKey: 'settings',
mockValue: 'mock settings'
}
])(
'submits $checkbox data when checkbox is selected',
'submits (%s) when the (%s) checkbox is selected',
async ({ checkbox, apiMethod, expectedKey, mockValue }) => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const { api } = (await import('@/scripts/api')) as any
vi.spyOn(api, apiMethod).mockResolvedValue(mockValue)
await findAndUpdateCheckbox(wrapper, checkbox)
const context = await submitForm(wrapper)
expect(context.extra[expectedKey]).toBe(mockValue)
const { captureMessage } = await import('@sentry/core')
// Select the checkbox
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
await checkboxes?.vm.$emit('update:modelValue', [checkbox])
await wrapper.vm.reportIssue()
expect(api[apiMethod]).toHaveBeenCalled()
// Verify the message includes the associated data
expect(captureMessage).toHaveBeenCalledWith(
'User reported issue',
expect.objectContaining({
extra: expect.objectContaining({ [expectedKey]: mockValue })
})
)
}
)
@@ -304,12 +207,24 @@ describe('ReportIssuePanel', () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const { app } = (await import('@/scripts/app')) as any
const { captureMessage } = await import('@sentry/core')
const mockWorkflow = { nodes: [], edges: [] }
vi.spyOn(app.graph, 'asSerialisable').mockReturnValue(mockWorkflow)
await findAndUpdateCheckbox(wrapper, 'Workflow')
const context = await submitForm(wrapper)
// Select the "Workflow" checkbox
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
await checkboxes?.vm.$emit('update:modelValue', ['Workflow'])
expect(context.extra.Workflow).toEqual(mockWorkflow)
await wrapper.vm.reportIssue()
expect(app.graph.asSerialisable).toHaveBeenCalled()
// Verify the message includes the workflow
expect(captureMessage).toHaveBeenCalledWith(
'User reported issue',
expect.objectContaining({
extra: expect.objectContaining({ workflow: mockWorkflow })
})
)
})
})

View File

@@ -13,13 +13,13 @@
optionValue="id"
/>
<Button
icon="pi pi-file-export"
icon="pi pi-upload"
text
:title="$t('g.export')"
@click="colorPaletteService.exportColorPalette(activePaletteId)"
/>
<Button
icon="pi pi-file-import"
icon="pi pi-download"
text
:title="$t('g.import')"
@click="importCustomPalette"

View File

@@ -43,6 +43,7 @@ import {
} from '@comfyorg/litegraph'
import { computed, onMounted, ref, watch, watchEffect } from 'vue'
import changelog from '@/assets/changelog.json'
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
import BottomPanel from '@/components/bottomPanel/BottomPanel.vue'
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
@@ -54,11 +55,11 @@ import SideToolbar from '@/components/sidebar/SideToolbar.vue'
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
import { CORE_SETTINGS } from '@/constants/coreSettings'
import { usePragmaticDroppable } from '@/hooks/dndHooks'
import { useWorkflowPersistence } from '@/hooks/workflowPersistenceHooks'
import { i18n } from '@/i18n'
import { api } from '@/scripts/api'
import { app as comfyApp } from '@/scripts/app'
import { app } from '@/scripts/app'
import { ChangeTracker } from '@/scripts/changeTracker'
import { getStorageValue, setStorageValue } from '@/scripts/utils'
import { IS_CONTROL_WIDGET, updateControlWidgetLabel } from '@/scripts/widgets'
import { useColorPaletteService } from '@/services/colorPaletteService'
import { useLitegraphService } from '@/services/litegraphService'
@@ -72,9 +73,11 @@ import {
} from '@/stores/modelToNodeStore'
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
import { useSettingStore } from '@/stores/settingStore'
import { useWorkflowStore } from '@/stores/workflowStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
import { getComfyVersion, isVersionLessThan } from '@/utils/envUtil'
const emit = defineEmits(['ready'])
const canvasRef = ref<HTMLCanvasElement | null>(null)
@@ -95,6 +98,29 @@ const canvasMenuEnabled = computed(() =>
)
const tooltipEnabled = computed(() => settingStore.get('Comfy.EnableTooltips'))
const storedWorkflows = JSON.parse(
getStorageValue('Comfy.OpenWorkflowsPaths') || '[]'
)
const storedActiveIndex = JSON.parse(
getStorageValue('Comfy.ActiveWorkflowIndex') || '-1'
)
const openWorkflows = computed(() => workspaceStore?.workflow?.openWorkflows)
const activeWorkflow = computed(() => workspaceStore?.workflow?.activeWorkflow)
const restoreState = computed<{ paths: string[]; activeIndex: number }>(() => {
if (!openWorkflows.value || !activeWorkflow.value) {
return { paths: [], activeIndex: -1 }
}
const paths = openWorkflows.value
.filter((workflow) => workflow?.isPersisted && !workflow.isModified)
.map((workflow) => workflow.path)
const activeIndex = openWorkflows.value.findIndex(
(workflow) => workflow.path === activeWorkflow.value?.path
)
return { paths, activeIndex }
})
watchEffect(() => {
const canvasInfoEnabled = settingStore.get('Comfy.Graph.CanvasInfo')
if (canvasStore.canvas) {
@@ -246,15 +272,57 @@ watch(
}
)
const workflowStore = useWorkflowStore()
const persistCurrentWorkflow = () => {
const workflow = JSON.stringify(comfyApp.serializeGraph())
localStorage.setItem('workflow', workflow)
if (api.clientId) {
sessionStorage.setItem(`workflow:${api.clientId}`, workflow)
}
}
const stopWatchChangeLog = watch(
() => workflowStore.activeWorkflow,
async () => {
if (!comfyAppReady.value) return
if (!changelog || settingStore.get('Comfy.ShowChangeLog') === false) {
stopWatchChangeLog()
return
}
const workflow = workflowStore.activeWorkflow
const activeState = workflow?.activeState
const comfyVersion = getComfyVersion() // TODO: initialize/fetch somewhere else
if (!workflow || !activeState || !comfyVersion) return
// Just checking if workflow temporary is not enough bc of Duplicate feature
const isBlank = !workflow.isPersisted && activeState.nodes?.length === 0
if (!isBlank) return
const lastShown = settingStore.get('Comfy.LastChangelogVersion')
const isSeen = lastShown && !isVersionLessThan(lastShown, comfyVersion)
if (!isSeen) {
app.loadGraphData(changelog)
settingStore.set('Comfy.LastChangelogVersion', comfyVersion)
}
stopWatchChangeLog()
}
)
watchEffect(() => {
LiteGraph.context_menu_scaling = settingStore.get(
'LiteGraph.ContextMenu.Scaling'
)
if (workflowStore.activeWorkflow) {
const workflow = workflowStore.activeWorkflow
setStorageValue('Comfy.PreviousWorkflow', workflow.key)
// When the activeWorkflow changes, the graph has already been loaded.
// Saving the current state of the graph to the localStorage.
persistCurrentWorkflow()
}
})
api.addEventListener('graphChanged', persistCurrentWorkflow)
usePragmaticDroppable(() => canvasRef.value, {
getDropEffect: (args): Exclude<DataTransfer['dropEffect'], 'none'> =>
args.source.data.type === 'tree-explorer-node' ? 'copy' : 'move',
onDrop: (event) => {
const loc = event.location.current.input
const dndData = event.source.data
@@ -312,20 +380,7 @@ usePragmaticDroppable(() => canvasRef.value, {
}
})
const loadCustomNodesI18n = async () => {
try {
const i18nData = await api.getCustomNodesI18n()
Object.entries(i18nData).forEach(([locale, message]) => {
i18n.global.mergeLocaleMessage(locale, message)
})
} catch (error) {
console.error('Failed to load custom nodes i18n', error)
}
}
const comfyAppReady = ref(false)
const workflowPersistence = useWorkflowPersistence()
onMounted(async () => {
// Backward compatible
// Assign all properties of lg to window
@@ -345,7 +400,6 @@ onMounted(async () => {
// ChangeTracker needs to be initialized before setup, as it will overwrite
// some listeners of litegraph canvas.
ChangeTracker.init(comfyApp)
await loadCustomNodesI18n()
await settingStore.loadSettingValues()
CORE_SETTINGS.forEach((setting) => {
settingStore.addSetting(setting)
@@ -365,9 +419,17 @@ onMounted(async () => {
'Comfy.CustomColorPalettes'
)
// Restore workflow and workflow tabs state from storage
await workflowPersistence.restorePreviousWorkflow()
workflowPersistence.restoreWorkflowTabsState()
const isRestorable = storedWorkflows?.length > 0 && storedActiveIndex >= 0
if (isRestorable)
workflowStore.openWorkflowsInBackground({
left: storedWorkflows.slice(0, storedActiveIndex),
right: storedWorkflows.slice(storedActiveIndex)
})
watch(restoreState, ({ paths, activeIndex }) => {
setStorageValue('Comfy.OpenWorkflowsPaths', JSON.stringify(paths))
setStorageValue('Comfy.ActiveWorkflowIndex', JSON.stringify(activeIndex))
})
// Start watching for locale change after the initial value is loaded.
watch(

View File

@@ -130,8 +130,8 @@ import ToggleSwitch from 'primevue/toggleswitch'
import { ref } from 'vue'
const showDialog = ref(false)
const autoUpdate = defineModel<boolean>('autoUpdate', { required: true })
const allowMetrics = defineModel<boolean>('allowMetrics', { required: true })
const autoUpdate = defineModel('autoUpdate', { required: true })
const allowMetrics = defineModel('allowMetrics', { required: true })
const showMetricsInfo = () => {
showDialog.value = true

View File

@@ -1,36 +0,0 @@
<template>
<Tag :icon :severity :value />
</template>
<script setup lang="ts">
import { PrimeIcons } from '@primevue/core/api'
import Tag from 'primevue/tag'
import { computed } from 'vue'
import { t } from '@/i18n'
// Properties
const props = defineProps<{
error: boolean
refreshing?: boolean
}>()
// Bindings
const icon = computed(() => {
if (props.refreshing) return PrimeIcons.QUESTION
if (props.error) return PrimeIcons.TIMES
return PrimeIcons.CHECK
})
const severity = computed(() => {
if (props.refreshing) return 'info'
if (props.error) return 'danger'
return 'success'
})
const value = computed(() => {
if (props.refreshing) return t('maintenance.refreshing')
if (props.error) return t('g.error')
return t('maintenance.OK')
})
</script>

View File

@@ -1,127 +0,0 @@
<template>
<div
class="task-div max-w-48 min-h-52 grid relative"
:class="{ 'opacity-75': isLoading }"
>
<Card
class="max-w-48 relative h-full overflow-hidden"
:class="{ 'opacity-65': runner.state !== 'error' }"
v-bind="(({ onClick, ...rest }) => rest)($attrs)"
>
<template #header>
<i
v-if="runner.state === 'error'"
class="pi pi-exclamation-triangle text-red-500 absolute m-2 top-0 -right-14 opacity-15"
style="font-size: 10rem"
/>
<img
v-if="task.headerImg"
:src="task.headerImg"
class="object-contain w-full h-full opacity-25 pt-4 px-4"
/>
</template>
<template #title>{{ task.name }}</template>
<template #content>{{ description }}</template>
<template #footer>
<div class="flex gap-4 mt-1">
<Button
:icon="task.button?.icon"
:label="task.button?.text"
class="w-full"
raised
icon-pos="right"
@click="(event) => $emit('execute', event)"
:loading="isExecuting"
/>
</div>
</template>
</Card>
<i
v-if="!isLoading && runner.state === 'OK'"
class="task-card-ok pi pi-check"
/>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Card from 'primevue/card'
import { computed } from 'vue'
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
import { useMinLoadingDurationRef } from '@/utils/refUtil'
const taskStore = useMaintenanceTaskStore()
const runner = computed(() => taskStore.getRunner(props.task))
// Properties
const props = defineProps<{
task: MaintenanceTask
}>()
// Events
defineEmits<{
execute: [event: MouseEvent]
}>()
// Bindings
const description = computed(() =>
runner.value.state === 'error'
? props.task.errorDescription ?? props.task.shortDescription
: props.task.shortDescription
)
// Use a minimum run time to ensure tasks "feel" like they have run
const reactiveLoading = computed(() => runner.value.refreshing)
const reactiveExecuting = computed(() => runner.value.executing)
const isLoading = useMinLoadingDurationRef(reactiveLoading, 250)
const isExecuting = useMinLoadingDurationRef(reactiveExecuting, 250)
</script>
<style scoped>
.task-card-ok {
@apply text-green-500 absolute -right-4 -bottom-4 opacity-100 row-span-full col-span-full transition-opacity;
font-size: 4rem;
text-shadow: 0.25rem 0 0.5rem black;
z-index: 10;
}
.p-card {
@apply transition-opacity;
--p-card-background: var(--p-button-secondary-background);
opacity: 0.9;
&.opacity-65 {
opacity: 0.4;
}
&:hover {
opacity: 1;
}
}
:deep(.p-card-header) {
z-index: 0;
}
:deep(.p-card-body) {
z-index: 1;
flex-grow: 1;
justify-content: space-between;
}
.task-div {
> i {
pointer-events: none;
}
&:hover > i {
opacity: 0.2;
}
}
</style>

View File

@@ -1,86 +0,0 @@
<template>
<tr
class="border-neutral-700 border-solid border-y"
:class="{
'opacity-50': runner.resolved,
'opacity-75': isLoading && runner.resolved
}"
>
<td class="text-center w-16">
<TaskListStatusIcon :state="runner.state" :loading="isLoading" />
</td>
<td>
<p class="inline-block">{{ task.name }}</p>
<Button
class="inline-block mx-2"
type="button"
:icon="PrimeIcons.INFO_CIRCLE"
severity="secondary"
:text="true"
@click="toggle"
/>
<Popover ref="infoPopover" class="block m-1 max-w-64 min-w-32">
<span class="whitespace-pre-line">{{ task.description }}</span>
</Popover>
</td>
<td class="text-right px-4">
<Button
:icon="task.button?.icon"
:label="task.button?.text"
:severity
icon-pos="right"
@click="(event) => $emit('execute', event)"
:loading="isExecuting"
/>
</td>
</tr>
</template>
<script setup lang="ts">
import { PrimeIcons } from '@primevue/core/api'
import Button from 'primevue/button'
import Popover from 'primevue/popover'
import { computed, ref } from 'vue'
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
import { VueSeverity } from '@/types/primeVueTypes'
import { useMinLoadingDurationRef } from '@/utils/refUtil'
import TaskListStatusIcon from './TaskListStatusIcon.vue'
const taskStore = useMaintenanceTaskStore()
const runner = computed(() => taskStore.getRunner(props.task))
// Properties
const props = defineProps<{
task: MaintenanceTask
}>()
// Events
defineEmits<{
execute: [event: MouseEvent]
}>()
// Binding
const severity = computed<VueSeverity>(() =>
runner.value.state === 'error' || runner.value.state === 'warning'
? 'primary'
: 'secondary'
)
// Use a minimum run time to ensure tasks "feel" like they have run
const reactiveLoading = computed(() => runner.value.refreshing)
const reactiveExecuting = computed(() => runner.value.executing)
const isLoading = useMinLoadingDurationRef(reactiveLoading, 250)
const isExecuting = useMinLoadingDurationRef(reactiveExecuting, 250)
// Popover
const infoPopover = ref()
const toggle = (event: Event) => {
infoPopover.value.toggle(event)
}
</script>

View File

@@ -1,115 +0,0 @@
<template>
<!-- Tasks -->
<section class="my-4">
<template v-if="filter.tasks.length === 0">
<!-- Empty filter -->
<Divider />
<p class="text-neutral-400 w-full text-center">
{{ $t('maintenance.allOk') }}
</p>
</template>
<template v-else>
<!-- Display: List -->
<table
v-if="displayAsList === PrimeIcons.LIST"
class="w-full border-collapse border-hidden"
>
<TaskListItem
v-for="task in filter.tasks"
:key="task.id"
:task
@execute="(event) => confirmButton(event, task)"
/>
</table>
<!-- Display: Cards -->
<template v-else>
<div class="flex flex-wrap justify-evenly gap-8 pad-y my-4">
<TaskCard
v-for="task in filter.tasks"
:key="task.id"
:task
@execute="(event) => confirmButton(event, task)"
/>
</div>
</template>
</template>
<ConfirmPopup />
</section>
</template>
<script setup lang="ts">
import { PrimeIcons } from '@primevue/core/api'
import { useConfirm, useToast } from 'primevue'
import ConfirmPopup from 'primevue/confirmpopup'
import Divider from 'primevue/divider'
import { t } from '@/i18n'
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
import type {
MaintenanceFilter,
MaintenanceTask
} from '@/types/desktop/maintenanceTypes'
import TaskCard from './TaskCard.vue'
import TaskListItem from './TaskListItem.vue'
const toast = useToast()
const confirm = useConfirm()
const taskStore = useMaintenanceTaskStore()
// Properties
const props = defineProps<{
displayAsList: string
filter: MaintenanceFilter
isRefreshing: boolean
}>()
const executeTask = async (task: MaintenanceTask) => {
let message: string | undefined
try {
// Success
if ((await taskStore.execute(task)) === true) return
message = t('maintenance.error.taskFailed')
} catch (error) {
message = (error as Error)?.message
}
toast.add({
severity: 'error',
summary: t('maintenance.error.toastTitle'),
detail: message ?? t('maintenance.error.defaultDescription'),
life: 10_000
})
}
// Commands
const confirmButton = async (event: MouseEvent, task: MaintenanceTask) => {
if (!task.requireConfirm) {
await executeTask(task)
return
}
confirm.require({
target: event.currentTarget as HTMLElement,
message: task.confirmText ?? t('maintenance.confirmTitle'),
icon: 'pi pi-exclamation-circle',
rejectProps: {
label: t('g.cancel'),
severity: 'secondary',
outlined: true
},
acceptProps: {
label: task.button?.text ?? t('g.save'),
severity: task.severity ?? 'primary'
},
// TODO: Not awaited.
accept: async () => {
await executeTask(task)
}
})
}
</script>

View File

@@ -1,44 +0,0 @@
<template>
<ProgressSpinner v-if="!state || loading" class="h-8 w-8" />
<template v-else>
<i :class="cssClasses" v-tooltip.top="{ value: tooltip, showDelay: 250 }" />
</template>
</template>
<script setup lang="ts">
import { PrimeIcons } from '@primevue/core/api'
import ProgressSpinner from 'primevue/progressspinner'
import { MaybeRef, computed } from 'vue'
import { t } from '@/i18n'
// Properties
const tooltip = computed(() => {
if (props.state === 'error') {
return t('g.error')
} else if (props.state === 'OK') {
return t('maintenance.OK')
} else {
return t('maintenance.Skipped')
}
})
const cssClasses = computed(() => {
let classes: string
if (props.state === 'error') {
classes = `${PrimeIcons.EXCLAMATION_TRIANGLE} text-red-500`
} else if (props.state === 'OK') {
classes = `${PrimeIcons.CHECK} text-green-500`
} else {
classes = PrimeIcons.MINUS
}
return `text-3xl pi ${classes}`
})
// Model
const props = defineProps<{
state: 'warning' | 'error' | 'resolved' | 'OK' | 'skipped' | undefined
loading?: MaybeRef<boolean>
}>()
</script>

View File

@@ -6,27 +6,11 @@ export default {
name: 'AutoCompletePlus',
extends: AutoComplete,
emits: ['focused-option-changed'],
data() {
return {
// Flag to determine if IME is active
isComposing: false
}
},
mounted() {
if (typeof AutoComplete.mounted === 'function') {
AutoComplete.mounted.call(this)
}
// Retrieve the actual <input> element and attach IME events
const inputEl = this.$el.querySelector('input')
if (inputEl) {
inputEl.addEventListener('compositionstart', () => {
this.isComposing = true
})
inputEl.addEventListener('compositionend', () => {
this.isComposing = false
})
}
// Add a watcher on the focusedOptionIndex property
this.$watch(
() => this.focusedOptionIndex,
@@ -35,18 +19,6 @@ export default {
this.$emit('focused-option-changed', newVal)
}
)
},
methods: {
// Override onKeyDown to block Enter when IME is active
onKeyDown(event) {
if (event.key === 'Enter' && this.isComposing) {
event.preventDefault()
event.stopPropagation()
return
}
AutoComplete.methods.onKeyDown.call(this, event)
}
}
}
</script>

View File

@@ -60,9 +60,8 @@
<!-- FilterAndValue -->
<template v-slot:chip="{ value }">
<SearchFilterChip
v-if="Array.isArray(value) && value.length === 2"
:key="`${value[0].id}-${value[1]}`"
@remove="onRemoveFilter($event, value as FilterAndValue)"
@remove="onRemoveFilter($event, value)"
:text="value[1]"
:badge="value[0].invokeSequence.toUpperCase()"
:badge-class="value[0].invokeSequence + '-badge'"

View File

@@ -33,7 +33,7 @@
<!-- Virtual top menu for native window (drag handle) -->
<div
v-show="isNativeWindow() && !showTopMenu"
v-show="isNativeWindow && !showTopMenu"
class="fixed top-0 left-0 app-drag w-full h-[var(--comfy-topbar-height)]"
/>
</template>
@@ -50,12 +50,7 @@ import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
import { app } from '@/scripts/app'
import { useSettingStore } from '@/stores/settingStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import {
electronAPI,
isElectron,
isNativeWindow,
showNativeMenu
} from '@/utils/envUtil'
import { electronAPI, isElectron, showNativeMenu } from '@/utils/envUtil'
const workspaceState = useWorkspaceStore()
const settingStore = useSettingStore()
@@ -69,6 +64,10 @@ const teleportTarget = computed(() =>
? '.comfyui-body-top'
: '.comfyui-body-bottom'
)
const isNativeWindow = computed(
() =>
isElectron() && settingStore.get('Comfy-Desktop.WindowStyle') === 'custom'
)
const showTopMenu = computed(
() => betaMenuEnabled.value && !workspaceState.focusMode
)

View File

@@ -19,9 +19,8 @@ export const CORE_MENU_COMMANDS = [
[
'Comfy.Help.OpenComfyUIIssues',
'Comfy.Help.OpenComfyUIDocs',
'Comfy.Help.OpenComfyOrgDiscord',
'Comfy.Help.OpenComfyUIForum'
'Comfy.Help.OpenComfyOrgDiscord'
]
],
[['Help'], ['Comfy.Help.AboutComfyUI', 'Comfy.Feedback']]
[['Help'], ['Comfy.Help.AboutComfyUI']]
]

View File

@@ -157,7 +157,7 @@ export const CORE_SETTINGS: SettingParams[] = [
id: 'Comfy.Workflow.ShowMissingModelsWarning',
name: 'Show missing models warning',
type: 'boolean',
defaultValue: true,
defaultValue: false,
experimental: true
},
{
@@ -290,7 +290,7 @@ export const CORE_SETTINGS: SettingParams[] = [
name: 'Node ID badge mode',
type: 'combo',
options: [NodeBadgeMode.None, NodeBadgeMode.ShowAll],
defaultValue: NodeBadgeMode.None
defaultValue: NodeBadgeMode.ShowAll
},
{
id: 'Comfy.NodeBadge.NodeLifeCycleBadgeMode',
@@ -710,17 +710,18 @@ export const CORE_SETTINGS: SettingParams[] = [
versionModified: '1.6.10'
},
{
id: 'Comfy.TutorialCompleted',
name: 'Tutorial completed',
type: 'hidden',
defaultValue: false,
versionAdded: '1.8.7'
id: 'Comfy.ShowChangeLog',
category: ['Comfy', 'Changelog'],
name: 'Show changelog on new release',
type: 'boolean',
defaultValue: true,
versionAdded: '1.7.15'
},
{
id: 'LiteGraph.ContextMenu.Scaling',
name: 'Scale node combo widget menus (lists) when zoomed in',
defaultValue: false,
type: 'boolean',
versionAdded: '1.8.8'
id: 'Comfy.LastChangelogVersion',
name: 'Last shown changelog version',
type: 'hidden',
defaultValue: '0.0.0',
versionAdded: '1.7.15'
}
]

View File

@@ -1,146 +0,0 @@
import { PrimeIcons } from '@primevue/core'
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
import { electronAPI } from '@/utils/envUtil'
const electron = electronAPI()
const openUrl = (url: string) => {
window.open(url, '_blank')
return true
}
export const DESKTOP_MAINTENANCE_TASKS: Readonly<MaintenanceTask>[] = [
{
id: 'basePath',
execute: async () => await electron.setBasePath(),
name: 'Base path',
shortDescription: 'Change the application base path.',
errorDescription: 'Unable to open the base path. Please select a new one.',
description:
'The base path is the default location where ComfyUI stores data. It is the location fo the python environment, and may also contain models, custom nodes, and other extensions.',
isInstallationFix: true,
button: {
icon: PrimeIcons.QUESTION,
text: 'Select'
}
},
{
id: 'git',
headerImg: '/assets/images/Git-Logo-White.svg',
execute: () => openUrl('https://git-scm.com/downloads/'),
name: 'Download git',
shortDescription: 'Open the git download page.',
errorDescription:
'Git is missing. Please download and install git, then restart ComfyUI Desktop.',
description:
'Git is required to download and manage custom nodes and other extensions. This task opens the download page in your default browser, where you can download the latest version of git. Once you have installed git, please restart ComfyUI Desktop.',
button: {
icon: PrimeIcons.EXTERNAL_LINK,
text: 'Download'
}
},
{
id: 'vcRedist',
execute: () => openUrl('https://aka.ms/vs/17/release/vc_redist.x64.exe'),
name: 'Download VC++ Redist',
shortDescription: 'Download the latest VC++ Redistributable runtime.',
description:
'The Visual C++ runtime libraries are required to run ComfyUI. You will need to download and install this file.',
button: {
icon: PrimeIcons.EXTERNAL_LINK,
text: 'Download'
}
},
{
id: 'reinstall',
severity: 'danger',
requireConfirm: true,
execute: async () => {
await electron.reinstall()
return true
},
name: 'Reinstall ComfyUI',
shortDescription:
'Deletes the desktop app config and load the welcome screen.',
description:
'Delete the desktop app config, restart the app, and load the installation screen.',
confirmText: 'Delete all saved config and reinstall?',
button: {
icon: PrimeIcons.EXCLAMATION_TRIANGLE,
text: 'Reinstall'
}
},
{
id: 'pythonPackages',
requireConfirm: true,
execute: async () => {
try {
await electron.uv.installRequirements()
return true
} catch (error) {
return false
}
},
name: 'Install python packages',
shortDescription:
'Installs the base python packages required to run ComfyUI.',
errorDescription:
'Python packages that are required to run ComfyUI are not installed.',
description:
'This will install the python packages required to run ComfyUI. This includes torch, torchvision, and other dependencies.',
usesTerminal: true,
isInstallationFix: true,
button: {
icon: PrimeIcons.DOWNLOAD,
text: 'Install'
}
},
{
id: 'uv',
execute: () =>
openUrl('https://docs.astral.sh/uv/getting-started/installation/'),
name: 'uv executable',
shortDescription: 'uv installs and maintains the python environment.',
description:
"This will open the download page for Astral's uv tool. uv is used to install python and manage python packages.",
button: {
icon: 'pi pi-asterisk',
text: 'Download'
}
},
{
id: 'uvCache',
severity: 'danger',
requireConfirm: true,
execute: async () => await electron.uv.clearCache(),
name: 'uv cache',
shortDescription: 'Remove the Astral uv cache of python packages.',
description:
'This will remove the uv cache directory and its contents. All downloaded python packages will need to be downloaded again.',
confirmText: 'Delete uv cache of python packages?',
isInstallationFix: true,
button: {
icon: PrimeIcons.TRASH,
text: 'Clear cache'
}
},
{
id: 'venvDirectory',
severity: 'danger',
requireConfirm: true,
execute: async () => await electron.uv.resetVenv(),
name: 'Reset virtual environment',
shortDescription:
'Remove and recreate the .venv directory. This removes all python packages.',
description:
'The python environment is where ComfyUI installs python and python packages. It is used to run the ComfyUI server.',
confirmText: 'Delete the .venv directory?',
usesTerminal: true,
isInstallationFix: true,
button: {
icon: PrimeIcons.FOLDER,
text: 'Recreate'
}
}
] as const

View File

@@ -1,8 +1,6 @@
import { t } from '@/i18n'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
import { useSettingStore } from '@/stores/settingStore'
import { useWorkflowStore } from '@/stores/workflowStore'
import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
;(async () => {
@@ -10,7 +8,6 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
const electronAPI = getElectronAPI()
const desktopAppVersion = await electronAPI.getElectronVersion()
const workflowStore = useWorkflowStore()
const onChangeRestartApp = (newValue: string, oldValue: string) => {
// Add a delay to allow changes to take effect before restarting.
@@ -42,18 +39,18 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
id: 'Comfy-Desktop.WindowStyle',
category: ['Comfy-Desktop', 'General', 'Window Style'],
name: 'Window Style',
tooltip: "Custom: Replace the system title bar with ComfyUI's Top menu",
tooltip: 'Choose custom option to hide the system title bar',
type: 'combo',
experimental: true,
defaultValue: 'default',
options: ['default', 'custom'],
onChange: (
newValue: 'default' | 'custom',
oldValue?: 'default' | 'custom'
oldValue: 'default' | 'custom'
) => {
if (!oldValue) return
electronAPI.Config.setWindowStyle(newValue)
onChangeRestartApp(newValue, oldValue)
}
}
],
@@ -115,6 +112,14 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
electronAPI.openDevTools()
}
},
{
id: 'Comfy-Desktop.OpenFeedbackPage',
label: 'Feedback',
icon: 'pi pi-envelope',
function() {
window.open('https://forum.comfy.org/c/v1-feedback/', '_blank')
}
},
{
id: 'Comfy-Desktop.OpenUserGuide',
label: 'Desktop User Guide',
@@ -144,32 +149,16 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
function() {
electronAPI.restartApp()
}
},
{
id: 'Comfy-Desktop.Quit',
label: 'Quit',
icon: 'pi pi-sign-out',
async function() {
// Confirm if unsaved workflows are open
if (workflowStore.modifiedWorkflows.length > 0) {
const confirmed = await useDialogService().confirm({
message: t('desktopMenu.confirmQuit'),
title: t('desktopMenu.quit'),
type: 'default'
})
if (!confirmed) return
}
electronAPI.quit()
}
}
],
menuCommands: [
{
path: ['Help'],
commands: ['Comfy-Desktop.OpenUserGuide']
commands: [
'Comfy-Desktop.OpenUserGuide',
'Comfy-Desktop.OpenFeedbackPage'
]
},
{
path: ['Help'],

File diff suppressed because it is too large Load Diff

View File

@@ -1,139 +0,0 @@
import type { IWidget } from '@comfyorg/litegraph'
import Load3d from '@/extensions/core/load3d/Load3d'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
import { api } from '@/scripts/api'
class Load3DConfiguration {
constructor(private load3d: Load3d) {}
configure(
loadFolder: 'input' | 'output',
modelWidget: IWidget,
material: IWidget,
bgColor: IWidget,
lightIntensity: IWidget,
upDirection: IWidget,
fov: IWidget,
cameraState?: any,
postModelUpdateFunc?: (load3d: Load3d) => void
) {
this.setupModelHandling(
modelWidget,
loadFolder,
cameraState,
postModelUpdateFunc
)
this.setupMaterial(material)
this.setupBackground(bgColor)
this.setupLighting(lightIntensity)
this.setupDirection(upDirection)
this.setupCamera(fov)
this.setupDefaultProperties()
}
private setupModelHandling(
modelWidget: IWidget,
loadFolder: 'input' | 'output',
cameraState?: any,
postModelUpdateFunc?: (load3d: Load3d) => void
) {
const onModelWidgetUpdate = this.createModelUpdateHandler(
loadFolder,
cameraState,
postModelUpdateFunc
)
if (modelWidget.value) {
onModelWidgetUpdate(modelWidget.value)
}
modelWidget.callback = onModelWidgetUpdate
}
private setupMaterial(material: IWidget) {
material.callback = (value: 'original' | 'normal' | 'wireframe') => {
this.load3d.setMaterialMode(value)
}
this.load3d.setMaterialMode(
material.value as 'original' | 'normal' | 'wireframe'
)
}
private setupBackground(bgColor: IWidget) {
bgColor.callback = (value: string) => {
this.load3d.setBackgroundColor(value)
}
this.load3d.setBackgroundColor(bgColor.value as string)
}
private setupLighting(lightIntensity: IWidget) {
lightIntensity.callback = (value: number) => {
this.load3d.setLightIntensity(value)
}
this.load3d.setLightIntensity(lightIntensity.value as number)
}
private setupDirection(upDirection: IWidget) {
upDirection.callback = (
value: 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z'
) => {
this.load3d.setUpDirection(value)
}
this.load3d.setUpDirection(
upDirection.value as 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z'
)
}
private setupCamera(fov: IWidget) {
fov.callback = (value: number) => {
this.load3d.setFOV(value)
}
this.load3d.setFOV(fov.value as number)
}
private setupDefaultProperties() {
const cameraType = this.load3d.loadNodeProperty(
'Camera Type',
'perspective'
)
this.load3d.toggleCamera(cameraType)
const showGrid = this.load3d.loadNodeProperty('Show Grid', true)
this.load3d.toggleGrid(showGrid)
}
private createModelUpdateHandler(
loadFolder: 'input' | 'output',
cameraState?: any,
postModelUpdateFunc?: (load3d: Load3d) => void
) {
let isFirstLoad = true
return async (value: string | number | boolean | object) => {
if (!value) return
const filename = value as string
const modelUrl = api.apiURL(
Load3dUtils.getResourceURL(
...Load3dUtils.splitFilePath(filename),
loadFolder
)
)
await this.load3d.loadModel(modelUrl, filename)
if (postModelUpdateFunc) {
postModelUpdateFunc(this.load3d)
}
if (isFirstLoad && cameraState && typeof cameraState === 'object') {
try {
this.load3d.setCameraState(cameraState)
} catch (error) {
console.warn('Failed to restore camera state:', error)
}
isFirstLoad = false
}
}
}
}
export default Load3DConfiguration

View File

@@ -1,945 +0,0 @@
import { LGraphNode } from '@comfyorg/litegraph'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
import { useToastStore } from '@/stores/toastStore'
class Load3d {
scene: THREE.Scene
perspectiveCamera: THREE.PerspectiveCamera
orthographicCamera: THREE.OrthographicCamera
activeCamera: THREE.Camera
renderer: THREE.WebGLRenderer
controls: OrbitControls
gltfLoader: GLTFLoader
objLoader: OBJLoader
mtlLoader: MTLLoader
fbxLoader: FBXLoader
stlLoader: STLLoader
currentModel: THREE.Object3D | null = null
originalModel: THREE.Object3D | THREE.BufferGeometry | GLTF | null = null
animationFrameId: number | null = null
gridHelper: THREE.GridHelper
lights: THREE.Light[] = []
clock: THREE.Clock
normalMaterial: THREE.MeshNormalMaterial
standardMaterial: THREE.MeshStandardMaterial
wireframeMaterial: THREE.MeshBasicMaterial
depthMaterial: THREE.MeshDepthMaterial
originalMaterials: WeakMap<THREE.Mesh, THREE.Material | THREE.Material[]> =
new WeakMap()
materialMode: 'original' | 'normal' | 'wireframe' | 'depth' = 'original'
currentUpDirection: 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z' =
'original'
originalRotation: THREE.Euler | null = null
viewHelper: ViewHelper = {} as ViewHelper
viewHelperContainer: HTMLDivElement = {} as HTMLDivElement
cameraSwitcherContainer: HTMLDivElement = {} as HTMLDivElement
gridSwitcherContainer: HTMLDivElement = {} as HTMLDivElement
node: LGraphNode = {} as LGraphNode
constructor(container: Element | HTMLElement) {
this.scene = new THREE.Scene()
this.perspectiveCamera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000)
this.perspectiveCamera.position.set(5, 5, 5)
const frustumSize = 10
this.orthographicCamera = new THREE.OrthographicCamera(
-frustumSize / 2,
frustumSize / 2,
frustumSize / 2,
-frustumSize / 2,
0.1,
1000
)
this.orthographicCamera.position.set(5, 5, 5)
this.activeCamera = this.perspectiveCamera
this.perspectiveCamera.lookAt(0, 0, 0)
this.orthographicCamera.lookAt(0, 0, 0)
this.renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true })
this.renderer.setSize(300, 300)
this.renderer.setClearColor(0x282828)
this.renderer.autoClear = false
const rendererDomElement: HTMLCanvasElement = this.renderer.domElement
container.appendChild(rendererDomElement)
this.controls = new OrbitControls(
this.activeCamera,
this.renderer.domElement
)
this.controls.enableDamping = true
this.controls.addEventListener('end', () => {
this.storeNodeProperty('Camera Info', this.getCameraState())
})
this.gltfLoader = new GLTFLoader()
this.objLoader = new OBJLoader()
this.mtlLoader = new MTLLoader()
this.fbxLoader = new FBXLoader()
this.stlLoader = new STLLoader()
this.clock = new THREE.Clock()
this.setupLights()
this.gridHelper = new THREE.GridHelper(10, 10)
this.gridHelper.position.set(0, 0, 0)
this.scene.add(this.gridHelper)
this.normalMaterial = new THREE.MeshNormalMaterial({
flatShading: false,
side: THREE.DoubleSide,
normalScale: new THREE.Vector2(1, 1),
transparent: false,
opacity: 1.0
})
this.wireframeMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff,
wireframe: true,
transparent: false,
opacity: 1.0
})
this.depthMaterial = new THREE.MeshDepthMaterial({
depthPacking: THREE.BasicDepthPacking,
side: THREE.DoubleSide
})
this.standardMaterial = this.createSTLMaterial()
this.createViewHelper(container)
this.createGridSwitcher(container)
this.createCameraSwitcher(container)
this.handleResize()
this.startAnimation()
}
setNode(node: LGraphNode) {
this.node = node
}
storeNodeProperty(name: string, value: any) {
if (this.node) {
this.node.properties[name] = value
}
}
loadNodeProperty(name: string, defaultValue: any) {
if (!this.node || !(name in this.node.properties)) {
return defaultValue
}
return this.node.properties[name]
}
createViewHelper(container: Element | HTMLElement) {
this.viewHelperContainer = document.createElement('div')
this.viewHelperContainer.style.position = 'absolute'
this.viewHelperContainer.style.bottom = '0'
this.viewHelperContainer.style.left = '0'
this.viewHelperContainer.style.width = '128px'
this.viewHelperContainer.style.height = '128px'
this.viewHelperContainer.addEventListener('pointerup', (event) => {
event.stopPropagation()
this.viewHelper.handleClick(event)
})
this.viewHelperContainer.addEventListener('pointerdown', (event) => {
event.stopPropagation()
})
container.appendChild(this.viewHelperContainer)
this.viewHelper = new ViewHelper(
this.activeCamera,
this.viewHelperContainer
)
this.viewHelper.center = this.controls.target
}
createGridSwitcher(container: Element | HTMLElement) {
this.gridSwitcherContainer = document.createElement('div')
this.gridSwitcherContainer.style.position = 'absolute'
this.gridSwitcherContainer.style.top = '28px' // 修改这里,让按钮在相机按钮下方
this.gridSwitcherContainer.style.left = '3px' // 与相机按钮左对齐
this.gridSwitcherContainer.style.width = '20px'
this.gridSwitcherContainer.style.height = '20px'
this.gridSwitcherContainer.style.cursor = 'pointer'
this.gridSwitcherContainer.style.alignItems = 'center'
this.gridSwitcherContainer.style.justifyContent = 'center'
this.gridSwitcherContainer.style.transition = 'background-color 0.2s'
const gridIcon = document.createElement('div')
gridIcon.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
<path d="M3 3h18v18H3z"/>
<path d="M3 9h18"/>
<path d="M3 15h18"/>
<path d="M9 3v18"/>
<path d="M15 3v18"/>
</svg>
`
const updateButtonState = () => {
if (this.gridHelper.visible) {
this.gridSwitcherContainer.style.backgroundColor =
'rgba(255, 255, 255, 0.2)'
} else {
this.gridSwitcherContainer.style.backgroundColor = 'transparent'
}
}
updateButtonState()
this.gridSwitcherContainer.addEventListener('mouseenter', () => {
if (!this.gridHelper.visible) {
this.gridSwitcherContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
}
})
this.gridSwitcherContainer.addEventListener('mouseleave', () => {
if (!this.gridHelper.visible) {
this.gridSwitcherContainer.style.backgroundColor = 'transparent'
}
})
this.gridSwitcherContainer.title = 'Toggle Grid'
this.gridSwitcherContainer.addEventListener('click', (event) => {
event.stopPropagation()
this.toggleGrid(!this.gridHelper.visible)
updateButtonState()
})
this.gridSwitcherContainer.appendChild(gridIcon)
container.appendChild(this.gridSwitcherContainer)
}
createCameraSwitcher(container: Element | HTMLElement) {
this.cameraSwitcherContainer = document.createElement('div')
this.cameraSwitcherContainer.style.position = 'absolute'
this.cameraSwitcherContainer.style.top = '3px'
this.cameraSwitcherContainer.style.left = '3px'
this.cameraSwitcherContainer.style.width = '20px'
this.cameraSwitcherContainer.style.height = '20px'
this.cameraSwitcherContainer.style.cursor = 'pointer'
this.cameraSwitcherContainer.style.alignItems = 'center'
this.cameraSwitcherContainer.style.justifyContent = 'center'
const cameraIcon = document.createElement('div')
cameraIcon.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
<path d="M18 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2Z"/>
<path d="m12 12 4-2.4"/>
<circle cx="12" cy="12" r="3"/>
</svg>
`
this.cameraSwitcherContainer.addEventListener('mouseenter', () => {
this.cameraSwitcherContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
})
this.cameraSwitcherContainer.addEventListener('mouseleave', () => {
this.cameraSwitcherContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.3)'
})
this.cameraSwitcherContainer.title =
'Switch Camera (Perspective/Orthographic)'
this.cameraSwitcherContainer.addEventListener('click', (event) => {
event.stopPropagation()
this.toggleCamera()
})
this.cameraSwitcherContainer.appendChild(cameraIcon)
container.appendChild(this.cameraSwitcherContainer)
}
setFOV(fov: number) {
if (this.activeCamera === this.perspectiveCamera) {
this.perspectiveCamera.fov = fov
this.perspectiveCamera.updateProjectionMatrix()
this.renderer.render(this.scene, this.activeCamera)
}
}
getCameraState() {
const currentType = this.getCurrentCameraType()
return {
position: this.activeCamera.position.clone(),
target: this.controls.target.clone(),
zoom:
this.activeCamera instanceof THREE.OrthographicCamera
? this.activeCamera.zoom
: (this.activeCamera as THREE.PerspectiveCamera).zoom,
cameraType: currentType
}
}
setCameraState(state: {
position: THREE.Vector3
target: THREE.Vector3
zoom: number
cameraType: 'perspective' | 'orthographic'
}) {
if (
this.activeCamera !==
(state.cameraType === 'perspective'
? this.perspectiveCamera
: this.orthographicCamera)
) {
//this.toggleCamera(state.cameraType)
}
this.activeCamera.position.copy(state.position)
this.controls.target.copy(state.target)
if (this.activeCamera instanceof THREE.OrthographicCamera) {
this.activeCamera.zoom = state.zoom
this.activeCamera.updateProjectionMatrix()
} else if (this.activeCamera instanceof THREE.PerspectiveCamera) {
this.activeCamera.zoom = state.zoom
this.activeCamera.updateProjectionMatrix()
}
this.controls.update()
}
setUpDirection(
direction: 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z'
) {
if (!this.currentModel) return
if (!this.originalRotation && this.currentModel.rotation) {
this.originalRotation = this.currentModel.rotation.clone()
}
this.currentUpDirection = direction
if (this.originalRotation) {
this.currentModel.rotation.copy(this.originalRotation)
}
switch (direction) {
case 'original':
break
case '-x':
this.currentModel.rotation.z = Math.PI / 2
break
case '+x':
this.currentModel.rotation.z = -Math.PI / 2
break
case '-y':
this.currentModel.rotation.x = Math.PI
break
case '+y':
break
case '-z':
this.currentModel.rotation.x = Math.PI / 2
break
case '+z':
this.currentModel.rotation.x = -Math.PI / 2
break
}
this.renderer.render(this.scene, this.activeCamera)
}
setMaterialMode(mode: 'original' | 'normal' | 'wireframe' | 'depth') {
this.materialMode = mode
if (this.currentModel) {
if (mode === 'depth') {
this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace
} else {
this.renderer.outputColorSpace = THREE.SRGBColorSpace
}
this.currentModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
switch (mode) {
case 'depth':
if (!this.originalMaterials.has(child)) {
this.originalMaterials.set(child, child.material)
}
const depthMat = new THREE.MeshDepthMaterial({
depthPacking: THREE.BasicDepthPacking,
side: THREE.DoubleSide
})
depthMat.onBeforeCompile = (shader) => {
shader.uniforms.cameraType = {
value:
this.activeCamera instanceof THREE.OrthographicCamera
? 1.0
: 0.0
}
shader.fragmentShader = `
uniform float cameraType;
${shader.fragmentShader}
`
shader.fragmentShader = shader.fragmentShader.replace(
/gl_FragColor\s*=\s*vec4\(\s*vec3\(\s*1.0\s*-\s*fragCoordZ\s*\)\s*,\s*opacity\s*\)\s*;/,
`
float depth = 1.0 - fragCoordZ;
if (cameraType > 0.5) {
depth = pow(depth, 400.0);
} else {
depth = pow(depth, 0.6);
}
gl_FragColor = vec4(vec3(depth), opacity);
`
)
}
depthMat.customProgramCacheKey = () => {
return this.activeCamera instanceof THREE.OrthographicCamera
? 'ortho'
: 'persp'
}
child.material = depthMat
break
case 'normal':
if (!this.originalMaterials.has(child)) {
this.originalMaterials.set(child, child.material)
}
child.material = new THREE.MeshNormalMaterial({
flatShading: false,
side: THREE.DoubleSide,
normalScale: new THREE.Vector2(1, 1),
transparent: false,
opacity: 1.0
})
child.geometry.computeVertexNormals()
break
case 'wireframe':
if (!this.originalMaterials.has(child)) {
this.originalMaterials.set(child, child.material)
}
child.material = new THREE.MeshBasicMaterial({
color: 0xffffff,
wireframe: true,
transparent: false,
opacity: 1.0
})
break
case 'original':
const originalMaterial = this.originalMaterials.get(child)
if (originalMaterial) {
child.material = originalMaterial
} else {
child.material = this.standardMaterial
}
break
}
}
})
this.renderer.render(this.scene, this.activeCamera)
}
}
setupLights() {
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
this.scene.add(ambientLight)
this.lights.push(ambientLight)
const mainLight = new THREE.DirectionalLight(0xffffff, 0.8)
mainLight.position.set(0, 10, 10)
this.scene.add(mainLight)
this.lights.push(mainLight)
const backLight = new THREE.DirectionalLight(0xffffff, 0.5)
backLight.position.set(0, 10, -10)
this.scene.add(backLight)
this.lights.push(backLight)
const leftFillLight = new THREE.DirectionalLight(0xffffff, 0.3)
leftFillLight.position.set(-10, 0, 0)
this.scene.add(leftFillLight)
this.lights.push(leftFillLight)
const rightFillLight = new THREE.DirectionalLight(0xffffff, 0.3)
rightFillLight.position.set(10, 0, 0)
this.scene.add(rightFillLight)
this.lights.push(rightFillLight)
const bottomLight = new THREE.DirectionalLight(0xffffff, 0.2)
bottomLight.position.set(0, -10, 0)
this.scene.add(bottomLight)
this.lights.push(bottomLight)
}
toggleCamera(cameraType?: 'perspective' | 'orthographic') {
const oldCamera = this.activeCamera
const position = oldCamera.position.clone()
const rotation = oldCamera.rotation.clone()
const target = this.controls.target.clone()
if (!cameraType) {
this.activeCamera =
oldCamera === this.perspectiveCamera
? this.orthographicCamera
: this.perspectiveCamera
} else {
this.activeCamera =
cameraType === 'perspective'
? this.perspectiveCamera
: this.orthographicCamera
if (oldCamera === this.activeCamera) {
return
}
}
this.activeCamera.position.copy(position)
this.activeCamera.rotation.copy(rotation)
if (this.materialMode === 'depth' && oldCamera !== this.activeCamera) {
this.setMaterialMode('depth')
}
this.controls.object = this.activeCamera
this.controls.target.copy(target)
this.controls.update()
this.viewHelper.dispose()
this.viewHelper = new ViewHelper(
this.activeCamera,
this.viewHelperContainer
)
this.viewHelper.center = this.controls.target
this.storeNodeProperty('Camera Type', this.getCurrentCameraType())
this.handleResize()
}
getCurrentCameraType(): 'perspective' | 'orthographic' {
return this.activeCamera === this.perspectiveCamera
? 'perspective'
: 'orthographic'
}
toggleGrid(showGrid: boolean) {
if (this.gridHelper) {
this.gridHelper.visible = showGrid
this.storeNodeProperty('Show Grid', showGrid)
}
}
setLightIntensity(intensity: number) {
this.lights.forEach((light) => {
if (light instanceof THREE.DirectionalLight) {
if (light === this.lights[1]) {
light.intensity = intensity * 0.8
} else if (light === this.lights[2]) {
light.intensity = intensity * 0.5
} else if (light === this.lights[5]) {
light.intensity = intensity * 0.2
} else {
light.intensity = intensity * 0.3
}
} else if (light instanceof THREE.AmbientLight) {
light.intensity = intensity * 0.5
}
})
}
startAnimation() {
const animate = () => {
this.animationFrameId = requestAnimationFrame(animate)
const delta = this.clock.getDelta()
if (this.viewHelper.animating) {
this.viewHelper.update(delta)
if (!this.viewHelper.animating) {
this.storeNodeProperty('Camera Info', this.getCameraState())
}
}
this.renderer.clear()
this.controls.update()
this.renderer.render(this.scene, this.activeCamera)
this.viewHelper.render(this.renderer)
}
animate()
}
clearModel() {
const objectsToRemove: THREE.Object3D[] = []
this.scene.traverse((object) => {
const isEnvironmentObject =
object === this.gridHelper ||
this.lights.includes(object as THREE.Light) ||
object === this.perspectiveCamera ||
object === this.orthographicCamera
if (!isEnvironmentObject) {
objectsToRemove.push(object)
}
})
objectsToRemove.forEach((obj) => {
if (obj.parent && obj.parent !== this.scene) {
obj.parent.remove(obj)
} else {
this.scene.remove(obj)
}
if (obj instanceof THREE.Mesh) {
obj.geometry?.dispose()
if (Array.isArray(obj.material)) {
obj.material.forEach((material) => material.dispose())
} else {
obj.material?.dispose()
}
}
})
this.resetScene()
}
protected resetScene() {
this.currentModel = null
this.originalRotation = null
const defaultDistance = 10
this.perspectiveCamera.position.set(
defaultDistance,
defaultDistance,
defaultDistance
)
this.orthographicCamera.position.set(
defaultDistance,
defaultDistance,
defaultDistance
)
this.perspectiveCamera.lookAt(0, 0, 0)
this.orthographicCamera.lookAt(0, 0, 0)
const frustumSize = 10
const aspect =
this.renderer.domElement.width / this.renderer.domElement.height
this.orthographicCamera.left = (-frustumSize * aspect) / 2
this.orthographicCamera.right = (frustumSize * aspect) / 2
this.orthographicCamera.top = frustumSize / 2
this.orthographicCamera.bottom = -frustumSize / 2
this.perspectiveCamera.updateProjectionMatrix()
this.orthographicCamera.updateProjectionMatrix()
this.controls.target.set(0, 0, 0)
this.controls.update()
this.renderer.render(this.scene, this.activeCamera)
this.materialMode = 'original'
this.originalMaterials = new WeakMap()
this.renderer.outputColorSpace = THREE.SRGBColorSpace
}
remove() {
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId)
}
this.controls.dispose()
this.viewHelper.dispose()
this.renderer.dispose()
this.renderer.domElement.remove()
this.scene.clear()
}
protected async loadModelInternal(
url: string,
fileExtension: string
): Promise<THREE.Object3D | null> {
let model: THREE.Object3D | null = null
switch (fileExtension) {
case 'stl':
const geometry = await this.stlLoader.loadAsync(url)
this.originalModel = geometry
geometry.computeVertexNormals()
const mesh = new THREE.Mesh(geometry, this.standardMaterial)
const group = new THREE.Group()
group.add(mesh)
model = group
break
case 'fbx':
const fbxModel = await this.fbxLoader.loadAsync(url)
this.originalModel = fbxModel
model = fbxModel
fbxModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
this.originalMaterials.set(child, child.material)
}
})
break
case 'obj':
if (this.materialMode === 'original') {
const mtlUrl = url.replace(/\.obj([^.]*$)/, '.mtl$1')
try {
const materials = await this.mtlLoader.loadAsync(mtlUrl)
materials.preload()
this.objLoader.setMaterials(materials)
} catch (e) {
console.log(
'No MTL file found or error loading it, continuing without materials'
)
}
}
model = await this.objLoader.loadAsync(url)
model.traverse((child) => {
if (child instanceof THREE.Mesh) {
this.originalMaterials.set(child, child.material)
}
})
break
case 'gltf':
case 'glb':
const gltf = await this.gltfLoader.loadAsync(url)
this.originalModel = gltf
model = gltf.scene
gltf.scene.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.geometry.computeVertexNormals()
this.originalMaterials.set(child, child.material)
}
})
break
}
return model
}
async loadModel(url: string, originalFileName?: string) {
try {
this.clearModel()
let fileExtension: string | undefined
if (originalFileName) {
fileExtension = originalFileName.split('.').pop()?.toLowerCase()
} else {
const filename = new URLSearchParams(url.split('?')[1]).get('filename')
fileExtension = filename?.split('.').pop()?.toLowerCase()
}
if (!fileExtension) {
useToastStore().addAlert('Could not determine file type')
return
}
let model = await this.loadModelInternal(url, fileExtension)
if (model) {
this.currentModel = model
await this.setupModel(model)
}
} catch (error) {
console.error('Error loading model:', error)
}
}
protected async setupModel(model: THREE.Object3D) {
const box = new THREE.Box3().setFromObject(model)
const size = box.getSize(new THREE.Vector3())
const center = box.getCenter(new THREE.Vector3())
const maxDim = Math.max(size.x, size.y, size.z)
const targetSize = 5
const scale = targetSize / maxDim
model.scale.multiplyScalar(scale)
box.setFromObject(model)
box.getCenter(center)
box.getSize(size)
model.position.set(-center.x, -box.min.y, -center.z)
this.scene.add(model)
if (this.materialMode !== 'original') {
this.setMaterialMode(this.materialMode)
}
if (this.currentUpDirection !== 'original') {
this.setUpDirection(this.currentUpDirection)
}
await this.setupCamera(size)
}
protected async setupCamera(size: THREE.Vector3) {
const distance = Math.max(size.x, size.z) * 2
const height = size.y * 2
this.perspectiveCamera.position.set(distance, height, distance)
this.orthographicCamera.position.set(distance, height, distance)
if (this.activeCamera === this.perspectiveCamera) {
this.perspectiveCamera.lookAt(0, size.y / 2, 0)
this.perspectiveCamera.updateProjectionMatrix()
} else {
const frustumSize = Math.max(size.x, size.y, size.z) * 2
const aspect =
this.renderer.domElement.width / this.renderer.domElement.height
this.orthographicCamera.left = (-frustumSize * aspect) / 2
this.orthographicCamera.right = (frustumSize * aspect) / 2
this.orthographicCamera.top = frustumSize / 2
this.orthographicCamera.bottom = -frustumSize / 2
this.orthographicCamera.lookAt(0, size.y / 2, 0)
this.orthographicCamera.updateProjectionMatrix()
}
this.controls.target.set(0, size.y / 2, 0)
this.controls.update()
this.renderer.outputColorSpace = THREE.SRGBColorSpace
this.renderer.toneMapping = THREE.ACESFilmicToneMapping
this.renderer.toneMappingExposure = 1
this.handleResize()
}
handleResize() {
const parentElement = this.renderer?.domElement?.parentElement
if (!parentElement) {
console.warn('Parent element not found')
return
}
const width = parentElement?.clientWidth
const height = parentElement?.clientHeight
if (this.activeCamera === this.perspectiveCamera) {
this.perspectiveCamera.aspect = width / height
this.perspectiveCamera.updateProjectionMatrix()
} else {
const frustumSize = 10
const aspect = width / height
this.orthographicCamera.left = (-frustumSize * aspect) / 2
this.orthographicCamera.right = (frustumSize * aspect) / 2
this.orthographicCamera.top = frustumSize / 2
this.orthographicCamera.bottom = -frustumSize / 2
this.orthographicCamera.updateProjectionMatrix()
}
this.renderer.setSize(width, height)
}
animate = () => {
requestAnimationFrame(this.animate)
this.controls.update()
this.renderer.render(this.scene, this.activeCamera)
}
captureScene(
width: number,
height: number
): Promise<{ scene: string; mask: string }> {
return new Promise(async (resolve, reject) => {
try {
const originalWidth = this.renderer.domElement.width
const originalHeight = this.renderer.domElement.height
const originalClearColor = this.renderer.getClearColor(
new THREE.Color()
)
const originalClearAlpha = this.renderer.getClearAlpha()
this.renderer.setSize(width, height)
if (this.activeCamera === this.perspectiveCamera) {
this.perspectiveCamera.aspect = width / height
this.perspectiveCamera.updateProjectionMatrix()
} else {
const frustumSize = 10
const aspect = width / height
this.orthographicCamera.left = (-frustumSize * aspect) / 2
this.orthographicCamera.right = (frustumSize * aspect) / 2
this.orthographicCamera.top = frustumSize / 2
this.orthographicCamera.bottom = -frustumSize / 2
this.orthographicCamera.updateProjectionMatrix()
}
this.renderer.clear()
this.renderer.render(this.scene, this.activeCamera)
const sceneData = this.renderer.domElement.toDataURL('image/png')
this.renderer.setClearColor(0x000000, 0)
this.renderer.clear()
this.renderer.render(this.scene, this.activeCamera)
const maskData = this.renderer.domElement.toDataURL('image/png')
this.renderer.setClearColor(originalClearColor, originalClearAlpha)
this.renderer.setSize(originalWidth, originalHeight)
this.handleResize()
resolve({ scene: sceneData, mask: maskData })
} catch (error) {
reject(error)
}
})
}
createSTLMaterial() {
return new THREE.MeshStandardMaterial({
color: 0x808080,
metalness: 0.1,
roughness: 0.8,
flatShading: false,
side: THREE.DoubleSide
})
}
setBackgroundColor(color: string) {
this.renderer.setClearColor(new THREE.Color(color))
this.renderer.render(this.scene, this.activeCamera)
}
}
export default Load3d

View File

@@ -1,304 +0,0 @@
import * as THREE from 'three'
import Load3d from '@/extensions/core/load3d/Load3d'
class Load3dAnimation extends Load3d {
currentAnimation: THREE.AnimationMixer | null = null
animationActions: THREE.AnimationAction[] = []
animationClips: THREE.AnimationClip[] = []
selectedAnimationIndex: number = 0
isAnimationPlaying: boolean = false
animationSpeed: number = 1.0
playPauseContainer: HTMLDivElement = {} as HTMLDivElement
animationSelect: HTMLSelectElement = {} as HTMLSelectElement
constructor(container: Element | HTMLElement) {
super(container)
this.createPlayPauseButton(container)
this.createAnimationList(container)
}
createAnimationList(container: Element | HTMLElement) {
this.animationSelect = document.createElement('select')
Object.assign(this.animationSelect.style, {
position: 'absolute',
top: '3px',
left: '50%',
transform: 'translateX(15px)',
width: '90px',
height: '20px',
backgroundColor: 'rgba(0, 0, 0, 0.3)',
color: 'white',
border: 'none',
borderRadius: '4px',
fontSize: '12px',
padding: '0 8px',
cursor: 'pointer',
display: 'none',
outline: 'none'
})
this.animationSelect.addEventListener('mouseenter', () => {
this.animationSelect.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
})
this.animationSelect.addEventListener('mouseleave', () => {
this.animationSelect.style.backgroundColor = 'rgba(0, 0, 0, 0.3)'
})
this.animationSelect.addEventListener('change', (event) => {
const select = event.target as HTMLSelectElement
this.updateSelectedAnimation(select.selectedIndex)
})
container.appendChild(this.animationSelect)
}
updateAnimationList() {
this.animationSelect.innerHTML = ''
this.animationClips.forEach((clip, index) => {
const option = document.createElement('option')
option.value = index.toString()
option.text = clip.name || `Animation ${index + 1}`
option.selected = index === this.selectedAnimationIndex
this.animationSelect.appendChild(option)
})
}
createPlayPauseButton(container: Element | HTMLElement) {
this.playPauseContainer = document.createElement('div')
this.playPauseContainer.style.position = 'absolute'
this.playPauseContainer.style.top = '3px'
this.playPauseContainer.style.left = '50%'
this.playPauseContainer.style.transform = 'translateX(-50%)'
this.playPauseContainer.style.width = '20px'
this.playPauseContainer.style.height = '20px'
this.playPauseContainer.style.cursor = 'pointer'
this.playPauseContainer.style.alignItems = 'center'
this.playPauseContainer.style.justifyContent = 'center'
const updateButtonState = () => {
const icon = this.playPauseContainer.querySelector('svg')
if (icon) {
if (this.isAnimationPlaying) {
icon.innerHTML = `
<path d="M6 4h4v16H6zM14 4h4v16h-4z"/>
`
this.playPauseContainer.title = 'Pause Animation'
} else {
icon.innerHTML = `
<path d="M8 5v14l11-7z"/>
`
this.playPauseContainer.title = 'Play Animation'
}
}
}
const playIcon = document.createElement('div')
playIcon.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
<path d="M8 5v14l11-7z"/>
</svg>
`
this.playPauseContainer.addEventListener('mouseenter', () => {
this.playPauseContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
})
this.playPauseContainer.addEventListener('mouseleave', () => {
this.playPauseContainer.style.backgroundColor = 'transparent'
})
this.playPauseContainer.addEventListener('click', (event) => {
event.stopPropagation()
this.toggleAnimation()
updateButtonState()
})
this.playPauseContainer.appendChild(playIcon)
container.appendChild(this.playPauseContainer)
this.playPauseContainer.style.display = 'none'
}
protected async setupModel(model: THREE.Object3D) {
await super.setupModel(model)
if (this.currentAnimation) {
this.currentAnimation.stopAllAction()
this.animationActions = []
}
let animations: THREE.AnimationClip[] = []
if (model.animations?.length > 0) {
animations = model.animations
} else if (this.originalModel && 'animations' in this.originalModel) {
animations = (
this.originalModel as unknown as { animations: THREE.AnimationClip[] }
).animations
}
if (animations.length > 0) {
this.animationClips = animations
if (model.type === 'Scene') {
this.currentAnimation = new THREE.AnimationMixer(model)
} else {
this.currentAnimation = new THREE.AnimationMixer(this.currentModel!)
}
if (this.animationClips.length > 0) {
this.updateSelectedAnimation(0)
}
}
if (this.animationClips.length > 0) {
this.playPauseContainer.style.display = 'block'
} else {
this.playPauseContainer.style.display = 'none'
}
if (this.animationClips.length > 0) {
this.playPauseContainer.style.display = 'block'
this.animationSelect.style.display = 'block'
this.updateAnimationList()
} else {
this.playPauseContainer.style.display = 'none'
this.animationSelect.style.display = 'none'
}
}
setAnimationSpeed(speed: number) {
this.animationSpeed = speed
this.animationActions.forEach((action) => {
action.setEffectiveTimeScale(speed)
})
}
updateSelectedAnimation(index: number) {
if (
!this.currentAnimation ||
!this.animationClips ||
index >= this.animationClips.length
) {
console.warn('Invalid animation update request')
return
}
this.animationActions.forEach((action) => {
action.stop()
})
this.currentAnimation.stopAllAction()
this.animationActions = []
this.selectedAnimationIndex = index
const clip = this.animationClips[index]
const action = this.currentAnimation.clipAction(clip)
action.setEffectiveTimeScale(this.animationSpeed)
action.reset()
action.clampWhenFinished = false
action.loop = THREE.LoopRepeat
if (this.isAnimationPlaying) {
action.play()
} else {
action.play()
action.paused = true
}
this.animationActions = [action]
this.updateAnimationList()
}
clearModel() {
if (this.currentAnimation) {
this.animationActions.forEach((action) => {
action.stop()
})
this.currentAnimation = null
}
this.animationActions = []
this.animationClips = []
this.selectedAnimationIndex = 0
this.isAnimationPlaying = false
this.animationSpeed = 1.0
super.clearModel()
if (this.animationSelect) {
this.animationSelect.style.display = 'none'
this.animationSelect.innerHTML = ''
}
}
getAnimationNames(): string[] {
return this.animationClips.map((clip, index) => {
return clip.name || `Animation ${index + 1}`
})
}
toggleAnimation(play?: boolean) {
if (!this.currentAnimation || this.animationActions.length === 0) {
console.warn('No animation to toggle')
return
}
this.isAnimationPlaying = play ?? !this.isAnimationPlaying
const icon = this.playPauseContainer.querySelector('svg')
if (icon) {
if (this.isAnimationPlaying) {
icon.innerHTML = '<path d="M6 4h4v16H6zM14 4h4v16h-4z"/>'
this.playPauseContainer.title = 'Pause Animation'
} else {
icon.innerHTML = '<path d="M8 5v14l11-7z"/>'
this.playPauseContainer.title = 'Play Animation'
}
}
this.animationActions.forEach((action) => {
if (this.isAnimationPlaying) {
action.paused = false
if (action.time === 0 || action.time === action.getClip().duration) {
action.reset()
}
} else {
action.paused = true
}
})
}
startAnimation() {
const animate = () => {
this.animationFrameId = requestAnimationFrame(animate)
const delta = this.clock.getDelta()
if (this.currentAnimation && this.isAnimationPlaying) {
this.currentAnimation.update(delta)
}
this.controls.update()
this.renderer.clear()
this.renderer.render(this.scene, this.activeCamera)
if (this.viewHelper.animating) {
this.viewHelper.update(delta)
if (!this.viewHelper.animating) {
this.storeNodeProperty('Camera Info', this.getCameraState())
}
}
this.viewHelper.render(this.renderer)
}
animate()
}
}
export default Load3dAnimation

View File

@@ -1,122 +0,0 @@
import Load3d from '@/extensions/core/load3d/Load3d'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useToastStore } from '@/stores/toastStore'
class Load3dUtils {
static async uploadTempImage(imageData: string, prefix: string) {
const blob = await fetch(imageData).then((r) => r.blob())
const name = `${prefix}_${Date.now()}.png`
const file = new File([blob], name)
const body = new FormData()
body.append('image', file)
body.append('subfolder', 'threed')
body.append('type', 'temp')
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
body
})
if (resp.status !== 200) {
const err = `Error uploading temp image: ${resp.status} - ${resp.statusText}`
useToastStore().addAlert(err)
throw new Error(err)
}
return await resp.json()
}
static async uploadFile(
load3d: Load3d,
file: File,
fileInput?: HTMLInputElement
) {
let uploadPath
try {
const body = new FormData()
body.append('image', file)
body.append('subfolder', '3d')
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
body
})
if (resp.status === 200) {
const data = await resp.json()
let path = data.name
if (data.subfolder) path = data.subfolder + '/' + path
uploadPath = path
const modelUrl = api.apiURL(
this.getResourceURL(...this.splitFilePath(path), 'input')
)
await load3d.loadModel(modelUrl, file.name)
const fileExt = file.name.split('.').pop()?.toLowerCase()
if (fileExt === 'obj' && fileInput?.files) {
try {
const mtlFile = Array.from(fileInput.files).find((f) =>
f.name.toLowerCase().endsWith('.mtl')
)
if (mtlFile) {
const mtlFormData = new FormData()
mtlFormData.append('image', mtlFile)
mtlFormData.append('subfolder', '3d')
await api.fetchApi('/upload/image', {
method: 'POST',
body: mtlFormData
})
}
} catch (mtlError) {
console.warn('Failed to upload MTL file:', mtlError)
}
}
} else {
useToastStore().addAlert(resp.status + ' - ' + resp.statusText)
}
} catch (error) {
console.error('Upload error:', error)
useToastStore().addAlert(
error instanceof Error ? error.message : 'Upload failed'
)
}
return uploadPath
}
static splitFilePath(path: string): [string, string] {
const folder_separator = path.lastIndexOf('/')
if (folder_separator === -1) {
return ['', path]
}
return [
path.substring(0, folder_separator),
path.substring(folder_separator + 1)
]
}
static getResourceURL(
subfolder: string,
filename: string,
type: string = 'input'
): string {
const params = [
'filename=' + encodeURIComponent(filename),
'type=' + type,
'subfolder=' + subfolder,
app.getRandParam().substring(1)
].join('&')
return `/view?${params}`
}
}
export default Load3dUtils

View File

@@ -782,27 +782,7 @@ app.registerExtension({
? origGetExtraMenuOptions.apply(this, arguments)
: undefined
const getPointerCanvasPos = () => {
const pos = this.graph?.list_of_graphcanvas?.at(0)?.graph_mouse
return pos ? { canvasX: pos[0], canvasY: pos[1] } : undefined
}
if (this.widgets) {
const { canvasX, canvasY } = getPointerCanvasPos()
const widget = this.getWidgetOnPos(canvasX, canvasY)
// @ts-expect-error custom widget type
if (widget && widget.type !== CONVERTED_TYPE) {
const config = getConfig.call(this, widget.name) ?? [
widget.type,
widget.options || {}
]
if (isConvertibleWidget(widget, config)) {
options.push({
content: `Convert ${widget.name} to input`,
callback: () => convertToInput(this, widget, config) && false
})
}
}
let toInput = []
let toWidget = []
for (const w of this.widgets) {

View File

@@ -9,7 +9,6 @@ import {
DEFAULT_DARK_COLOR_PALETTE,
DEFAULT_LIGHT_COLOR_PALETTE
} from '@/constants/coreColorPalettes'
import { t } from '@/i18n'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
@@ -539,32 +538,6 @@ export function useCoreCommands(): ComfyCommand[] {
if (workflowStore.activeWorkflow)
workflowService.closeWorkflow(workflowStore.activeWorkflow)
}
},
{
id: 'Comfy.Feedback',
icon: 'pi pi-megaphone',
label: 'Give Feedback',
versionAdded: '1.8.2',
function: () => {
dialogService.showIssueReportDialog({
title: t('g.feedback'),
subtitle: t('issueReport.feedbackTitle'),
panelProps: {
errorType: 'Feedback',
defaultFields: ['SystemStats', 'Settings']
}
})
}
},
{
id: 'Comfy.Help.OpenComfyUIForum',
icon: 'pi pi-comments',
label: 'Open ComfyUI Forum',
menubarLabel: 'ComfyUI Forum',
versionAdded: '1.8.2',
function: () => {
window.open('https://forum.comfy.org/', '_blank')
}
}
]
}

View File

@@ -1,130 +0,0 @@
import { computed, watch, watchEffect } from 'vue'
import { api } from '@/scripts/api'
import { app as comfyApp } from '@/scripts/app'
import { getStorageValue, setStorageValue } from '@/scripts/utils'
import { useWorkflowService } from '@/services/workflowService'
import { useModelStore } from '@/stores/modelStore'
import { useSettingStore } from '@/stores/settingStore'
import { useWorkflowStore } from '@/stores/workflowStore'
export function useWorkflowPersistence() {
const workflowStore = useWorkflowStore()
const settingStore = useSettingStore()
const persistCurrentWorkflow = () => {
const workflow = JSON.stringify(comfyApp.serializeGraph())
localStorage.setItem('workflow', workflow)
if (api.clientId) {
sessionStorage.setItem(`workflow:${api.clientId}`, workflow)
}
}
const loadWorkflowFromStorage = async (
json: string | null,
workflowName: string | null
) => {
if (!json) return false
const workflow = JSON.parse(json)
await comfyApp.loadGraphData(workflow, true, true, workflowName)
return true
}
const loadPreviousWorkflowFromStorage = async () => {
const workflowName = getStorageValue('Comfy.PreviousWorkflow')
const clientId = api.initialClientId ?? api.clientId
// Try loading from session storage first
if (clientId) {
const sessionWorkflow = sessionStorage.getItem(`workflow:${clientId}`)
if (await loadWorkflowFromStorage(sessionWorkflow, workflowName)) {
return true
}
}
// Fall back to local storage
const localWorkflow = localStorage.getItem('workflow')
return await loadWorkflowFromStorage(localWorkflow, workflowName)
}
const loadDefaultWorkflow = async () => {
if (!settingStore.get('Comfy.TutorialCompleted')) {
await settingStore.set('Comfy.TutorialCompleted', true)
await useModelStore().loadModelFolders()
await useWorkflowService().loadTutorialWorkflow()
} else {
await comfyApp.loadGraphData()
}
}
const restorePreviousWorkflow = async () => {
try {
const restored = await loadPreviousWorkflowFromStorage()
if (!restored) {
await loadDefaultWorkflow()
}
} catch (err) {
console.error('Error loading previous workflow', err)
await loadDefaultWorkflow()
}
}
// Setup watchers
watchEffect(() => {
if (workflowStore.activeWorkflow) {
const workflow = workflowStore.activeWorkflow
setStorageValue('Comfy.PreviousWorkflow', workflow.key)
// When the activeWorkflow changes, the graph has already been loaded.
// Saving the current state of the graph to the localStorage.
persistCurrentWorkflow()
}
})
api.addEventListener('graphChanged', persistCurrentWorkflow)
// Restore workflow tabs states
const openWorkflows = computed(() => workflowStore.openWorkflows)
const activeWorkflow = computed(() => workflowStore.activeWorkflow)
const restoreState = computed<{ paths: string[]; activeIndex: number }>(
() => {
if (!openWorkflows.value || !activeWorkflow.value) {
return { paths: [], activeIndex: -1 }
}
const paths = openWorkflows.value
.filter((workflow) => workflow?.isPersisted && !workflow.isModified)
.map((workflow) => workflow.path)
const activeIndex = openWorkflows.value.findIndex(
(workflow) => workflow.path === activeWorkflow.value?.path
)
return { paths, activeIndex }
}
)
// Get storage values before setting watchers
const storedWorkflows = JSON.parse(
getStorageValue('Comfy.OpenWorkflowsPaths') || '[]'
)
const storedActiveIndex = JSON.parse(
getStorageValue('Comfy.ActiveWorkflowIndex') || '-1'
)
watch(restoreState, ({ paths, activeIndex }) => {
setStorageValue('Comfy.OpenWorkflowsPaths', JSON.stringify(paths))
setStorageValue('Comfy.ActiveWorkflowIndex', JSON.stringify(activeIndex))
})
const restoreWorkflowTabsState = () => {
const isRestorable = storedWorkflows?.length > 0 && storedActiveIndex >= 0
if (isRestorable) {
workflowStore.openWorkflowsInBackground({
left: storedWorkflows.slice(0, storedActiveIndex),
right: storedWorkflows.slice(storedActiveIndex)
})
}
}
return {
restorePreviousWorkflow,
restoreWorkflowTabsState
}
}

View File

@@ -20,12 +20,12 @@
"Comfy-Desktop_OpenDevTools": {
"label": "Open DevTools"
},
"Comfy-Desktop_OpenFeedbackPage": {
"label": "Feedback"
},
"Comfy-Desktop_OpenUserGuide": {
"label": "Desktop User Guide"
},
"Comfy-Desktop_Quit": {
"label": "Quit"
},
"Comfy-Desktop_Reinstall": {
"label": "Reinstall"
},
@@ -83,9 +83,6 @@
"Comfy_ExportWorkflowAPI": {
"label": "Export Workflow (API Format)"
},
"Comfy_Feedback": {
"label": "Give Feedback"
},
"Comfy_Graph_FitGroupToContents": {
"label": "Fit Group To Contents"
},
@@ -110,9 +107,6 @@
"Comfy_Help_OpenComfyUIDocs": {
"label": "Open ComfyUI Docs"
},
"Comfy_Help_OpenComfyUIForum": {
"label": "Open ComfyUI Forum"
},
"Comfy_Help_OpenComfyUIIssues": {
"label": "Open ComfyUI Issues"
},

View File

@@ -72,24 +72,17 @@
"export": "Export",
"workflow": "Workflow",
"success": "Success",
"ok": "OK",
"feedback": "Feedback"
"ok": "OK"
},
"issueReport": {
"submitErrorReport": "Submit Error Report (Optional)",
"provideEmail": "Give us your email (optional)",
"provideEmail": "Give us your email (Optional)",
"provideAdditionalDetails": "Provide additional details (optional)",
"stackTrace": "Stack Trace",
"systemStats": "System Stats",
"contactFollowUp": "Contact me for follow up",
"notifyResolve": "Notify me when resolved",
"helpFix": "Help Fix This",
"rating": "Rating",
"feedbackTitle": "Help us improve ComfyUI by providing feedback",
"validation": {
"maxLength": "Message too long",
"invalidEmail": "Please enter a valid email address"
}
"helpFix": "Help Fix This"
},
"color": {
"default": "Default",
@@ -366,8 +359,8 @@
"Open Models Folder": "Open Models Folder",
"Open Outputs Folder": "Open Outputs Folder",
"Open DevTools": "Open DevTools",
"Feedback": "Feedback",
"Desktop User Guide": "Desktop User Guide",
"Quit": "Quit",
"Reinstall": "Reinstall",
"Restart": "Restart",
"Browse Templates": "Browse Templates",
@@ -387,7 +380,6 @@
"Duplicate Current Workflow": "Duplicate Current Workflow",
"Export": "Export",
"Export (API)": "Export (API)",
"Give Feedback": "Give Feedback",
"Fit Group To Contents": "Fit Group To Contents",
"Group Selected Nodes": "Group Selected Nodes",
"Convert selected nodes to group node": "Convert selected nodes to group node",
@@ -396,7 +388,6 @@
"About ComfyUI": "About ComfyUI",
"Comfy-Org Discord": "Comfy-Org Discord",
"ComfyUI Docs": "ComfyUI Docs",
"ComfyUI Forum": "ComfyUI Forum",
"ComfyUI Issues": "ComfyUI Issues",
"Interrupt": "Interrupt",
"Load Default Workflow": "Load Default Workflow",
@@ -427,9 +418,7 @@
},
"desktopMenu": {
"reinstall": "Reinstall",
"confirmReinstall": "This will clear your extra_models_config.yaml file,\nand begin installation again.\n\nAre you sure?",
"quit": "Quit",
"confirmQuit": "There are unsaved workflows open; any unsaved changes will be lost. Ignore this and quit?"
"confirmReinstall": "This will clear your extra_models_config.yaml file,\nand begin installation again.\n\nAre you sure?"
},
"settingsCategories": {
"Comfy-Desktop": "Comfy-Desktop",
@@ -471,8 +460,7 @@
"Server-Config": "Server-Config",
"About": "About",
"EditTokenWeight": "Edit Token Weight",
"CustomColorPalettes": "Custom Color Palettes",
"ContextMenu": "Context Menu"
"CustomColorPalettes": "Custom Color Palettes"
},
"serverConfigItems": {
"listen": {
@@ -693,26 +681,5 @@
"UPSCALE_MODEL": "UPSCALE_MODEL",
"VAE": "VAE",
"WEBCAM": "WEBCAM"
},
"maintenance": {
"allOk": "No issues were detected.",
"status": "Status",
"detected": "Detected",
"refreshing": "Refreshing",
"None": "None",
"OK": "OK",
"Skipped": "Skipped",
"showManual": "Show maintenance tasks",
"confirmTitle": "Are you sure?",
"error": {
"toastTitle": "Task error",
"taskFailed": "Task failed to run.",
"defaultDescription": "An error occurred while running a maintenance task."
}
},
"missingModelsDialog": {
"doNotAskAgain": "Don't show this again",
"missingModels": "Missing Models",
"missingModelsMessage": "When loading the graph, the following models were not found"
}
}

View File

@@ -7,7 +7,7 @@
},
"Comfy-Desktop_WindowStyle": {
"name": "Window Style",
"tooltip": "Custom: Replace the system title bar with ComfyUI's Top menu",
"tooltip": "Choose custom option to hide the system title bar",
"options": {
"default": "default",
"custom": "custom"
@@ -309,9 +309,6 @@
"name": "Maxium FPS",
"tooltip": "The maximum frames per second that the canvas is allowed to render. Caps GPU usage at the cost of smoothness. If 0, the screen refresh rate is used. Default: 0"
},
"LiteGraph_ContextMenu_Scaling": {
"name": "Scale node combo widget menus (lists) when zoomed in"
},
"pysssss_SnapToGrid": {
"name": "Always snap to grid"
}

View File

@@ -20,12 +20,12 @@
"Comfy-Desktop_OpenDevTools": {
"label": "Ouvrir les outils de développement"
},
"Comfy-Desktop_OpenFeedbackPage": {
"label": "Retour d'information"
},
"Comfy-Desktop_OpenUserGuide": {
"label": "Guide de l'utilisateur du bureau"
},
"Comfy-Desktop_Quit": {
"label": "Quitter"
},
"Comfy-Desktop_Reinstall": {
"label": "Réinstaller"
},
@@ -83,9 +83,6 @@
"Comfy_ExportWorkflowAPI": {
"label": "Exporter le flux de travail (format API)"
},
"Comfy_Feedback": {
"label": "Retour d'information"
},
"Comfy_Graph_FitGroupToContents": {
"label": "Ajuster le groupe au contenu"
},
@@ -110,9 +107,6 @@
"Comfy_Help_OpenComfyUIDocs": {
"label": "Ouvrir les documents ComfyUI"
},
"Comfy_Help_OpenComfyUIForum": {
"label": "Ouvrir le forum Comfy-Org"
},
"Comfy_Help_OpenComfyUIIssues": {
"label": "Ouvrir les problèmes ComfyUI"
},

View File

@@ -43,9 +43,7 @@
"WEBCAM": "WEBCAM"
},
"desktopMenu": {
"confirmQuit": "Il y a des flux de travail non enregistrés ouverts; toutes les modifications non enregistrées seront perdues. Ignorer cela et quitter?",
"confirmReinstall": "Cela effacera votre fichier extra_models_config.yaml,\net commencera l'installation à nouveau.\n\nÊtes-vous sûr ?",
"quit": "Quitter",
"reinstall": "Réinstaller"
},
"downloadGit": {
@@ -89,7 +87,6 @@
"experimental": "BETA",
"export": "Exportation",
"extensionName": "Nom de l'extension",
"feedback": "Commentaires",
"findIssues": "Trouver des problèmes",
"firstTimeUIMessage": "C'est la première fois que vous utilisez la nouvelle interface utilisateur. Choisissez \"Menu > Utiliser le nouveau menu > Désactivé\" pour restaurer l'ancienne interface utilisateur.",
"goToNode": "Aller au nœud",
@@ -247,35 +244,13 @@
},
"issueReport": {
"contactFollowUp": "Contactez-moi pour un suivi",
"feedbackTitle": "Aidez-nous à améliorer ComfyUI en fournissant des commentaires",
"helpFix": "Aidez à résoudre cela",
"notifyResolve": "Prévenez-moi lorsque résolu",
"provideAdditionalDetails": "Fournir des détails supplémentaires (facultatif)",
"provideEmail": "Donnez-nous votre email (Facultatif)",
"rating": "Évaluation",
"stackTrace": "Trace de la pile",
"submitErrorReport": "Soumettre un rapport d'erreur (Facultatif)",
"systemStats": "Statistiques du système",
"validation": {
"invalidEmail": "Veuillez entrer une adresse e-mail valide",
"maxLength": "Message trop long"
}
},
"maintenance": {
"None": "Aucun",
"OK": "OK",
"Skipped": "Ignoré",
"allOk": "Aucun problème détecté.",
"confirmTitle": "Êtes-vous sûr ?",
"detected": "Détecté",
"error": {
"defaultDescription": "Une erreur s'est produite lors de l'exécution d'une tâche de maintenance.",
"taskFailed": "La tâche a échoué.",
"toastTitle": "Erreur de tâche"
},
"refreshing": "Actualisation",
"showManual": "Afficher les tâches de maintenance",
"status": "Statut"
"systemStats": "Statistiques du système"
},
"menu": {
"autoQueue": "File d'attente automatique",
@@ -313,7 +288,6 @@
"Collapse/Expand Selected Nodes": "Réduire/Étendre les nœuds sélectionnés",
"Comfy-Org Discord": "Discord de Comfy-Org",
"ComfyUI Docs": "Docs de ComfyUI",
"ComfyUI Forum": "Forum ComfyUI",
"ComfyUI Issues": "Problèmes de ComfyUI",
"Convert selected nodes to group node": "Convertir les nœuds sélectionnés en nœud de groupe",
"Desktop User Guide": "Guide de l'utilisateur de bureau",
@@ -321,9 +295,9 @@
"Edit": "Éditer",
"Export": "Exporter",
"Export (API)": "Exporter (API)",
"Feedback": "Retour d'information",
"Fit Group To Contents": "Ajuster le groupe au contenu",
"Fit view to selected nodes": "Ajuster la vue aux nœuds sélectionnés",
"Give Feedback": "Donnez votre avis",
"Group Selected Nodes": "Grouper les nœuds sélectionnés",
"Help": "Aide",
"Interrupt": "Interrompre",
@@ -345,7 +319,6 @@
"Previous Opened Workflow": "Flux de travail ouvert précédent",
"Queue Prompt": "Invite de file d'attente",
"Queue Prompt (Front)": "Invite de file d'attente (Front)",
"Quit": "Quitter",
"Redo": "Refaire",
"Refresh Node Definitions": "Actualiser les définitions de nœud",
"Reinstall": "Réinstaller",
@@ -370,11 +343,6 @@
"Zoom In": "Zoom avant",
"Zoom Out": "Zoom arrière"
},
"missingModelsDialog": {
"doNotAskAgain": "Ne plus afficher ce message",
"missingModels": "Modèles manquants",
"missingModelsMessage": "Lors du chargement du graphique, les modèles suivants n'ont pas été trouvés"
},
"nodeCategories": {
"3d": "3d",
"3d_models": "modèles_3d",
@@ -603,7 +571,6 @@
"ColorPalette": "Palette de Couleurs",
"Comfy": "Confort",
"Comfy-Desktop": "Comfy-Desktop",
"ContextMenu": "Menu Contextuel",
"CustomColorPalettes": "Palettes de Couleurs Personnalisées",
"DevMode": "Mode Développeur",
"EditTokenWeight": "Modifier le Poids du Jeton",

View File

@@ -309,9 +309,6 @@
"name": "FPS maximum",
"tooltip": "Le nombre maximum d'images par seconde que le canevas est autorisé à rendre. Limite l'utilisation du GPU au détriment de la fluidité. Si 0, le taux de rafraîchissement de l'écran est utilisé. Par défaut : 0"
},
"LiteGraph_ContextMenu_Scaling": {
"name": "Mise à l'échelle des menus de widgets combinés de nœuds (listes) lors du zoom"
},
"pysssss_SnapToGrid": {
"name": "Toujours aligner sur la grille"
}

View File

@@ -20,12 +20,12 @@
"Comfy-Desktop_OpenDevTools": {
"label": "DevToolsを開く"
},
"Comfy-Desktop_OpenFeedbackPage": {
"label": "フィードバック"
},
"Comfy-Desktop_OpenUserGuide": {
"label": "デスクトップユーザーガイド"
},
"Comfy-Desktop_Quit": {
"label": "終了"
},
"Comfy-Desktop_Reinstall": {
"label": "再インストール"
},
@@ -83,9 +83,6 @@
"Comfy_ExportWorkflowAPI": {
"label": "ワークフローをエクスポートAPI形式"
},
"Comfy_Feedback": {
"label": "フィードバック"
},
"Comfy_Graph_FitGroupToContents": {
"label": "グループを内容に合わせて調整"
},
@@ -110,9 +107,6 @@
"Comfy_Help_OpenComfyUIDocs": {
"label": "ComfyUIのドキュメントを開く"
},
"Comfy_Help_OpenComfyUIForum": {
"label": "Comfy-Orgフォーラムを開く"
},
"Comfy_Help_OpenComfyUIIssues": {
"label": "ComfyUIの問題を開く"
},

View File

@@ -43,9 +43,7 @@
"WEBCAM": "ウェブカメラ"
},
"desktopMenu": {
"confirmQuit": "保存されていないワークフローが開いています。保存されていない変更はすべて失われます。これを無視して終了しますか?",
"confirmReinstall": "これにより、extra_models_config.yamlファイルがクリアされ、再インストールが開始されます。本当によろしいですか",
"quit": "終了",
"reinstall": "再インストール"
},
"downloadGit": {
@@ -89,7 +87,6 @@
"experimental": "ベータ",
"export": "エクスポート",
"extensionName": "拡張機能名",
"feedback": "フィードバック",
"findIssues": "問題を見つける",
"firstTimeUIMessage": "新しいUIを初めて使用しています。「メニュー > 新しいメニューを使用 > 無効」を選択して古いUIに戻してください。",
"goToNode": "ノードに移動",
@@ -247,35 +244,13 @@
},
"issueReport": {
"contactFollowUp": "フォローアップのために私に連絡する",
"feedbackTitle": "フィードバックを提供してComfyUIの改善にご協力ください",
"helpFix": "これを修正するのを助ける",
"notifyResolve": "解決したときに通知する",
"provideAdditionalDetails": "追加の詳細を提供する(オプション)",
"provideEmail": "あなたのメールアドレスを教えてください(オプション)",
"rating": "評価",
"stackTrace": "スタックトレース",
"submitErrorReport": "エラーレポートを提出する(オプション)",
"systemStats": "システム統計",
"validation": {
"invalidEmail": "有効なメールアドレスを入力してください",
"maxLength": "メッセージが長すぎます"
}
},
"maintenance": {
"None": "なし",
"OK": "OK",
"Skipped": "スキップされました",
"allOk": "問題は検出されませんでした。",
"confirmTitle": "よろしいですか?",
"detected": "検出されました",
"error": {
"defaultDescription": "メンテナンスタスクの実行中にエラーが発生しました。",
"taskFailed": "タスクの実行に失敗しました。",
"toastTitle": "タスクエラー"
},
"refreshing": "更新中",
"showManual": "メンテナンスタスクを表示",
"status": "ステータス"
"systemStats": "システム統計"
},
"menu": {
"autoQueue": "自動キュー",
@@ -313,7 +288,6 @@
"Collapse/Expand Selected Nodes": "選択したノードの折りたたみ/展開",
"Comfy-Org Discord": "Comfy-Org Discord",
"ComfyUI Docs": "ComfyUIのドキュメント",
"ComfyUI Forum": "ComfyUI フォーラム",
"ComfyUI Issues": "ComfyUIの問題",
"Convert selected nodes to group node": "選択したノードをグループノードに変換",
"Desktop User Guide": "デスクトップユーザーガイド",
@@ -321,9 +295,9 @@
"Edit": "編集",
"Export": "エクスポート",
"Export (API)": "エクスポート (API)",
"Feedback": "フィードバック",
"Fit Group To Contents": "グループを内容に合わせる",
"Fit view to selected nodes": "選択したノードにビューを合わせる",
"Give Feedback": "フィードバックを送る",
"Group Selected Nodes": "選択したノードをグループ化",
"Help": "ヘルプ",
"Interrupt": "中断",
@@ -345,7 +319,6 @@
"Previous Opened Workflow": "前に開いたワークフロー",
"Queue Prompt": "キューのプロンプト",
"Queue Prompt (Front)": "キューのプロンプト (前面)",
"Quit": "終了",
"Redo": "やり直す",
"Refresh Node Definitions": "ノード定義を更新",
"Reinstall": "再インストール",
@@ -370,11 +343,6 @@
"Zoom In": "ズームイン",
"Zoom Out": "ズームアウト"
},
"missingModelsDialog": {
"doNotAskAgain": "再度表示しない",
"missingModels": "モデルが見つかりません",
"missingModelsMessage": "グラフを読み込む際に、次のモデルが見つかりませんでした"
},
"nodeCategories": {
"3d": "3d",
"3d_models": "3Dモデル",
@@ -603,7 +571,6 @@
"ColorPalette": "カラーパレット",
"Comfy": "Comfy",
"Comfy-Desktop": "Comfyデスクトップ",
"ContextMenu": "コンテキストメニュー",
"CustomColorPalettes": "カスタムカラーパレット",
"DevMode": "開発モード",
"EditTokenWeight": "トークンの重みを編集",

View File

@@ -309,9 +309,6 @@
"name": "最大FPS",
"tooltip": "キャンバスがレンダリングできる最大フレーム数です。スムーズさの代わりにGPU使用量を制限します。0の場合、画面のリフレッシュレートが使用されます。デフォルト0"
},
"LiteGraph_ContextMenu_Scaling": {
"name": "ズームイン時にノードコンボウィジェットメニュー(リスト)をスケーリングする"
},
"pysssss_SnapToGrid": {
"name": "常にグリッドにスナップ"
}

View File

@@ -20,12 +20,12 @@
"Comfy-Desktop_OpenDevTools": {
"label": "DevTools 열기"
},
"Comfy-Desktop_OpenFeedbackPage": {
"label": "피드백"
},
"Comfy-Desktop_OpenUserGuide": {
"label": "데스크톱 사용자 가이드"
},
"Comfy-Desktop_Quit": {
"label": "종료"
},
"Comfy-Desktop_Reinstall": {
"label": "재설치"
},
@@ -83,9 +83,6 @@
"Comfy_ExportWorkflowAPI": {
"label": "워크플로 내보내기 (API 형식)"
},
"Comfy_Feedback": {
"label": "피드백"
},
"Comfy_Graph_FitGroupToContents": {
"label": "그룹을 내용에 맞게 맞추기"
},
@@ -110,9 +107,6 @@
"Comfy_Help_OpenComfyUIDocs": {
"label": "ComfyUI 문서 열기"
},
"Comfy_Help_OpenComfyUIForum": {
"label": "Comfy-Org 포럼 열기"
},
"Comfy_Help_OpenComfyUIIssues": {
"label": "ComfyUI 문제 열기"
},

View File

@@ -43,9 +43,7 @@
"WEBCAM": "웹캠"
},
"desktopMenu": {
"confirmQuit": "저장되지 않은 워크플로가 열려 있습니다. 저장되지 않은 변경 사항은 모두 손실됩니다. 이를 무시하고 종료하시겠습니까?",
"confirmReinstall": "이 작업은 extra_models_config.yaml 파일을 지우고 설치를 다시 시작합니다. 정말로 진행하시겠습니까?",
"quit": "종료",
"reinstall": "재설치"
},
"downloadGit": {
@@ -89,7 +87,6 @@
"experimental": "베타",
"export": "내보내기",
"extensionName": "확장 이름",
"feedback": "피드백",
"findIssues": "문제 찾기",
"firstTimeUIMessage": "새 UI를 처음 사용합니다. \"메뉴 > 새 메뉴 사용 > 비활성화\"를 선택하여 이전 UI로 복원하세요.",
"goToNode": "노드로 이동",
@@ -138,7 +135,7 @@
"terminal": "터미널",
"upload": "업로드",
"videoFailedToLoad": "비디오를 로드하지 못했습니다.",
"workflow": "워크플로"
"workflow": "워크플로"
},
"graphCanvasMenu": {
"fitView": "보기 맞춤",
@@ -203,13 +200,13 @@
"title": "수동 구성",
"virtualEnvironmentPath": "가상 환경 경로"
},
"metricsDisabled": "데이터 수집 비활성화",
"metricsEnabled": "데이터 수집 활성화",
"metricsDisabled": "메트릭스 비활성화",
"metricsEnabled": "메트릭스 활성화",
"migrateFromExistingInstallation": "기존 설치에서 마이그레이션",
"migration": "마이그레이션",
"migrationOptional": "마이그레이션은 선택 사항입니다. 기존에 설치된 것이 없다면, 이 단계를 건너뛸 수 있습니다.",
"migrationSourcePathDescription": "기존에 설치된 ComfyUI가 있으면, 기존 사용자 파일과 모델을 새 설치본으로 복사하거나 링크 할 수 있습니다. 기존의 ComfyUI 설치는 영향을 받지 않습니다.",
"moreInfo": "더 많은 정보를 원하시면, 다음을 읽어주세요",
"migrationSourcePathDescription": "기존 ComfyUI 설치가 있으면, 기존 사용자 파일과 모델을 새 설치로 복사/링크할 수 있습니다. 기존의 ComfyUI 설치는 영향을 받지 않습니다.",
"moreInfo": "자세한 정보는 우리의",
"parentMissing": "경로가 존재하지 않습니다 - 먼저 포함하는 디렉토리를 생성하세요",
"pathExists": "디렉토리가 이미 존재합니다 - 모든 데이터를 백업했는지 확인해 주세요",
"pathValidationFailed": "경로 유효성 검사 실패",
@@ -217,65 +214,43 @@
"selectItemsToMigrate": "마이그레이션 항목 선택",
"settings": {
"allowMetrics": "사용 통계",
"allowMetricsDescription": "익명의 사용 통계를 보내 ComfyUI를 개선하는 데 도움을 줍니다. 개인 정보나 워크플로 내용은 수집되지 않습니다.",
"allowMetricsDescription": "익명의 사용 통계를 보내 ComfyUI를 개선하는 데 도움을 줍니다. 개인 정보나 워크플로 내용은 수집되지 않습니다.",
"autoUpdate": "자동 업데이트",
"autoUpdateDescription": "업데이트가 가능해지면 자동으로 다운로드하고 설치합니다. 업데이트가 설치되기 전에 항상 알림을 받습니다.",
"dataCollectionDialog": {
"collect": {
"errorReports": "오류 메시지 및 스택 추적",
"systemInfo": "하드웨어, OS 유형, 앱 버전",
"userJourneyEvents": "사용자 행동 흐름 이벤트"
"userJourneyEvents": "사용자 여정 이벤트"
},
"doNotCollect": {
"customNodeConfigurations": "커스텀 노드 구성",
"customNodeConfigurations": "사용자 정의 노드 구성",
"fileSystemInformation": "파일 시스템 정보",
"personalInformation": "개인 정보",
"workflowContents": "워크플로 내용"
"workflowContents": "워크플로 내용"
},
"title": "데이터 수집 안내",
"viewFullPolicy": "전체 정책 보기",
"whatWeCollect": "수집하는 정보:",
"whatWeDoNotCollect": "수집하지 않는 정보:"
},
"errorUpdatingConsent": "데이터 수집 동의 설정 업데이트 오류",
"errorUpdatingConsentDetail": "데이터 수집 동의 설정 업데이트에 실패했습니다",
"errorUpdatingConsent": "동의 업데이트 오류",
"errorUpdatingConsentDetail": "메트릭스 동의 설정 업데이트에 실패했습니다",
"learnMoreAboutData": "데이터 수집에 대해 더 알아보기"
},
"systemLocations": "시스템 위치",
"unhandledError": "알 수 없는 오류",
"updateConsent": "이전에 충돌 보고에 동의하셨습니다. 이제 버그를 식별하고 앱을 개선하기 위해 이벤트 기반 통계 정보의 추적을 시작합니다. 개인 식별할 수 있는 정보는 수집지 않습니다."
"updateConsent": "당신은 이전에 충돌 보고에 동의습니다. 이제 버그를 식별하고 앱을 개선하기 위해 이벤트 기반 메트릭스를 추적하고 있습니다. 개인 식별 정보는 수집지 않습니다."
},
"issueReport": {
"contactFollowUp": "추적 조사를 위해 연락해 주세요",
"feedbackTitle": "피드백을 제공함으로써 ComfyUI를 개선하는 데 도움을 주십시오",
"helpFix": "이 문제 해결에 도움을 주세요",
"notifyResolve": "해결되었을 때 알려주세요",
"provideAdditionalDetails": "추가 세부 사항 제공 (선택 사항)",
"provideEmail": "이메일을 알려주세요 (선택 사항)",
"rating": "평가",
"stackTrace": "스택 추적",
"submitErrorReport": "오류 보고서 제출 (선택 사항)",
"systemStats": "시스템 통계",
"validation": {
"invalidEmail": "유효한 이메일 주소를 입력해 주세요",
"maxLength": "메시지가 너무 깁니다"
}
},
"maintenance": {
"None": "없음",
"OK": "확인",
"Skipped": "건너뜀",
"allOk": "문제가 발견되지 않았습니다.",
"confirmTitle": "확실합니까?",
"detected": "감지됨",
"error": {
"defaultDescription": "유지 보수 작업을 실행하는 동안 오류가 발생했습니다.",
"taskFailed": "작업 실행에 실패했습니다.",
"toastTitle": "작업 오류"
},
"refreshing": "새로 고침 중",
"showManual": "유지 보수 작업 보기",
"status": "상태"
"systemStats": "시스템 통계"
},
"menu": {
"autoQueue": "자동 실행 큐",
@@ -309,21 +284,20 @@
"Clear Pending Tasks": "보류 중인 작업 제거하기",
"Clear Workflow": "워크플로 지우기",
"Clipspace": "클립스페이스",
"Close Current Workflow": "현재 워크플로 닫기",
"Close Current Workflow": "현재 워크플로 닫기",
"Collapse/Expand Selected Nodes": "선택한 노드 축소/확장",
"Comfy-Org Discord": "Comfy-Org 디스코드",
"ComfyUI Docs": "ComfyUI 문서",
"ComfyUI Forum": "ComfyUI 포럼",
"ComfyUI Issues": "ComfyUI 이슈 페이지",
"Convert selected nodes to group node": "선택한 노드를 그룹 노드로 변환",
"Desktop User Guide": "데스크톱 사용자 가이드",
"Duplicate Current Workflow": "현재 워크플로 복제",
"Duplicate Current Workflow": "현재 워크플로 복제",
"Edit": "편집",
"Export": "내보내기",
"Export (API)": "내보내기 (API)",
"Feedback": "피드백",
"Fit Group To Contents": "그룹을 내용에 맞게 조정",
"Fit view to selected nodes": "선택한 노드에 맞게 보기 조정",
"Give Feedback": "피드백 제공",
"Group Selected Nodes": "선택한 노드 그룹화",
"Help": "도움말",
"Interrupt": "중단",
@@ -345,7 +319,6 @@
"Previous Opened Workflow": "이전 열린 워크플로",
"Queue Prompt": "실행 큐에 프롬프트 추가",
"Queue Prompt (Front)": "실행 큐 맨 앞에 프롬프트 추가",
"Quit": "종료",
"Redo": "다시 실행",
"Refresh Node Definitions": "노드 정의 새로 고침",
"Reinstall": "재설치",
@@ -370,11 +343,6 @@
"Zoom In": "확대",
"Zoom Out": "축소"
},
"missingModelsDialog": {
"doNotAskAgain": "다시 보지 않기",
"missingModels": "모델이 없습니다",
"missingModelsMessage": "그래프를 로드할 때 다음 모델을 찾을 수 없었습니다"
},
"nodeCategories": {
"3d": "3d",
"3d_models": "3D 모델",
@@ -603,7 +571,6 @@
"ColorPalette": "색상 팔레트",
"Comfy": "Comfy",
"Comfy-Desktop": "Comfy-Desktop",
"ContextMenu": "컨텍스트 메뉴",
"CustomColorPalettes": "사용자 정의 색상 팔레트",
"DevMode": "개발자 모드",
"EditTokenWeight": "토큰 가중치 편집",

View File

@@ -12,7 +12,7 @@
"name": "노이즈"
},
"sigmas": {
"name": "시그마 배열"
"name": "시그마"
}
}
},
@@ -1425,10 +1425,10 @@
}
},
"FlipSigmas": {
"display_name": "시그마 배열 뒤집기",
"display_name": "시그마 뒤집기",
"inputs": {
"sigmas": {
"name": "시그마 배열"
"name": "시그마"
}
}
},
@@ -4567,7 +4567,7 @@
"name": "샘플러"
},
"sigmas": {
"name": "시그마 배열"
"name": "시그마"
}
},
"outputs": {
@@ -4595,7 +4595,7 @@
"name": "샘플러"
},
"sigmas": {
"name": "시그마 배열"
"name": "시그마"
}
},
"outputs": {
@@ -4875,7 +4875,7 @@
"name": "시그마"
},
"sigmas": {
"name": "시그마 배열"
"name": "시그마"
}
}
},
@@ -4983,10 +4983,10 @@
}
},
"SplitSigmas": {
"display_name": "시그마 배열 분할 (스텝)",
"display_name": "시그마 분할 (스텝)",
"inputs": {
"sigmas": {
"name": "시그마 배열"
"name": "시그마"
},
"step": {
"name": "분할 스텝"
@@ -4994,29 +4994,29 @@
},
"outputs": {
"0": {
"name": "높은 시그마 배열"
"name": "높은 시그마"
},
"1": {
"name": "낮은 시그마 배열"
"name": "낮은 시그마"
}
}
},
"SplitSigmasDenoise": {
"display_name": "시그마 배열 분할 (노이즈 제거양)",
"display_name": "시그마 분할 (노이즈 제거양)",
"inputs": {
"denoise": {
"name": "노이즈 제거양"
},
"sigmas": {
"name": "시그마 배열"
"name": "시그마"
}
},
"outputs": {
"0": {
"name": "높은 시그마 배열"
"name": "높은 시그마"
},
"1": {
"name": "낮은 시그마 배열"
"name": "낮은 시그마"
}
}
},

View File

@@ -309,9 +309,6 @@
"name": "최대 FPS",
"tooltip": "캔버스가 렌더링할 수 있는 최대 프레임 수입니다. 부드럽게 동작하도록 GPU 사용률을 제한 합니다. 0이면 화면 주사율로 작동 합니다. 기본값: 0"
},
"LiteGraph_ContextMenu_Scaling": {
"name": "확대시 노드 콤보 위젯 메뉴 (목록) 스케일링"
},
"pysssss_SnapToGrid": {
"name": "항상 그리드에 스냅"
}

View File

@@ -1,31 +1,31 @@
{
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
"label": "Открыть папку пользовательских нод"
"label": "Открыть папку с пользовательскими узлами"
},
"Comfy-Desktop_Folders_OpenInputsFolder": {
"label": "Открыть папку входных данных"
"label": "Открыть папку с входными данными"
},
"Comfy-Desktop_Folders_OpenLogsFolder": {
"label": "Открыть папку логов"
"label": "Открыть папку с логами"
},
"Comfy-Desktop_Folders_OpenModelConfig": {
"label": "Открыть extra_model_paths.yaml"
},
"Comfy-Desktop_Folders_OpenModelsFolder": {
"label": "Открыть папку моделей"
"label": "Открыть папку с моделями"
},
"Comfy-Desktop_Folders_OpenOutputsFolder": {
"label": "Открыть папку результатов"
"label": "Открыть папку с результатами"
},
"Comfy-Desktop_OpenDevTools": {
"label": "Открыть инструменты разработчика"
},
"Comfy-Desktop_OpenFeedbackPage": {
"label": "Обратная связь"
},
"Comfy-Desktop_OpenUserGuide": {
"label": "Руководство пользователя для рабочего стола"
},
"Comfy-Desktop_Quit": {
"label": "Выйти"
},
"Comfy-Desktop_Reinstall": {
"label": "Переустановить"
},
@@ -36,7 +36,7 @@
"label": "Просмотр шаблонов"
},
"Comfy_Canvas_FitView": {
"label": "Подогнать вид к выбранным нодам"
"label": "Подогнать вид к выбранным узлам"
},
"Comfy_Canvas_ResetView": {
"label": "Сбросить вид"
@@ -48,19 +48,19 @@
"label": "Переключить блокировку холста"
},
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
"label": "Обход/Необход выбранных нод"
"label": "Обход/Необход выбранных узлов"
},
"Comfy_Canvas_ToggleSelectedNodes_Collapse": {
"label": "Свернуть/Развернуть выбранные ноды"
"label": "Свернуть/Развернуть выбранные узлы"
},
"Comfy_Canvas_ToggleSelectedNodes_Mute": {
"label": "Отключить/Включить звук выбранных нод"
"label": "Отключить/Включить звук выбранных узлов"
},
"Comfy_Canvas_ToggleSelectedNodes_Pin": {
"label": "Закрепить/Открепить выбранные ноды"
"label": "Закрепить/Открепить выбранные узлы"
},
"Comfy_Canvas_ToggleSelected_Pin": {
"label": "Закрепить/Открепить выбранных нод"
"label": "Закрепить/Открепить выбранные элементы"
},
"Comfy_Canvas_ZoomIn": {
"label": "Увеличить"
@@ -83,38 +83,32 @@
"Comfy_ExportWorkflowAPI": {
"label": "Экспорт рабочего процесса (формат API)"
},
"Comfy_Feedback": {
"label": "Обратная связь"
},
"Comfy_Graph_FitGroupToContents": {
"label": "Подогнать группу к содержимому"
},
"Comfy_Graph_GroupSelectedNodes": {
"label": "Группировать выбранные ноды"
"label": "Группировать выбранные узлы"
},
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
"label": "Преобразовать выбранные ноды в групповую ноду"
"label": "Преобразовать выбранные узлы в групповой узел"
},
"Comfy_GroupNode_ManageGroupNodes": {
"label": "Управление групповыми нодами"
"label": "Управление групповыми узлами"
},
"Comfy_GroupNode_UngroupSelectedGroupNodes": {
"label": "Разгруппировать выбранные групповые ноды"
"label": "Разгруппировать выбранные групповые узлы"
},
"Comfy_Help_AboutComfyUI": {
"label": "Открыть «О ComfyUI»"
"label": "Открыть о ComfyUI"
},
"Comfy_Help_OpenComfyOrgDiscord": {
"label": "Открыть Comfy-Org Discord"
},
"Comfy_Help_OpenComfyUIDocs": {
"label": "Открыть документацию ComfyUI"
},
"Comfy_Help_OpenComfyUIForum": {
"label": "Открыть форум Comfy-Org"
"label": "Открыть документы ComfyUI"
},
"Comfy_Help_OpenComfyUIIssues": {
"label": "Открыть ComfyUI Issues"
"label": "Открыть проблемы ComfyUI"
},
"Comfy_Interrupt": {
"label": "Прервать"
@@ -141,7 +135,7 @@
"label": "Повторить"
},
"Comfy_RefreshNodeDefinitions": {
"label": "Обновить определения нод"
"label": "Обновить определения узлов"
},
"Comfy_SaveWorkflow": {
"label": "Сохранить рабочий процесс"
@@ -153,7 +147,7 @@
"label": "Показать диалог настроек"
},
"Comfy_ToggleTheme": {
"label": "Переключить тему (Тёмная/Светлая)"
"label": "Переключить тему (Темная/Светлая)"
},
"Comfy_Undo": {
"label": "Отменить"
@@ -177,7 +171,7 @@
"label": "Переключить нижнюю панель терминала"
},
"Workspace_ToggleBottomPanelTab_logs-terminal": {
"label": "Переключить нижнюю панель логов"
"label": "Переключить нижнюю панель журналов"
},
"Workspace_ToggleFocusMode": {
"label": "Переключить режим фокуса"
@@ -187,8 +181,8 @@
"tooltip": "Библиотека моделей"
},
"Workspace_ToggleSidebarTab_node-library": {
"label": "Переключить боковую панель библиотеки нод",
"tooltip": "Библиотека нод"
"label": "Переключить боковую панель библиотеки узлов",
"tooltip": "Библиотека узлов"
},
"Workspace_ToggleSidebarTab_queue": {
"label": "Переключить боковую панель очереди",

View File

@@ -3,10 +3,10 @@
"blue": "Синий",
"custom": "Пользовательский",
"default": "По умолчанию",
"green": "Зелёный",
"green": "Зеленый",
"pink": "Розовый",
"red": "Красный",
"yellow": ёлтый"
"yellow": елтый"
},
"dataTypes": {
"AUDIO": "АУДИО",
@@ -16,17 +16,17 @@
"CLIP_VISION_OUTPUT": "CLIP_VISION_OUTPUT",
"COMBO": "КОМБО",
"CONDITIONING": "КОНДИЦИОНИРОВАНИЕ",
"CONTROL_NET": "CONTROL_NET",
"CONTROL_NET": "СЕТЬ УПРАВЛЕНИЯ",
"FLOAT": "ПЛАВАЮЩИЙ",
"FLOATS": "ПЛАВАЮЩИЕ",
"GLIGEN": "GLIGEN",
"GUIDER": "ГИД",
"HOOKS": "ХУКИ",
"HOOK_KEYFRAMES": "КЛЮЧЕВЫЕ_КАДРЫ_ХУКА",
"GUIDER": "РУКОВОДИТЕЛЬ",
"HOOKS": "КРЮКИ",
"HOOK_KEYFRAMES": "КЛЮЧЕВЫЕ КАДРЫ КРЮКА",
"IMAGE": "ИЗОБРАЖЕНИЕ",
"INT": "ЦЕЛОЕ",
"LATENT": "ЛАТЕНТНЫЙ",
"LATENT_OPERATION": "ЛАТЕНТНАЯ_ОПЕРАЦИЯ",
"LATENT_OPERATION": "ЛАТЕНТНАЯ ОПЕРАЦИЯ",
"LOAD_3D": "ЗАГРУЗИТЬ_3D",
"LOAD_3D_ANIMATION": "ЗАГРУЗИТЬ_3D_АНИМАЦИЮ",
"MASK": "МАСКА",
@@ -36,25 +36,23 @@
"SAMPLER": "СЭМПЛЕР",
"SIGMAS": "СИГМЫ",
"STRING": "СТРОКА",
"STYLE_MODEL": "МОДЕЛЬ_СТИЛЯ",
"TIMESTEPS_RANGE": "ДИАПАЗОН_ВРЕМЕННЫХ_ШАГОВ",
"UPSCALE_MODEL": "МОДЕЛЬ_АПСКЕЙЛА",
"STYLE_MODEL": "МОДЕЛЬ СТИЛЯ",
"TIMESTEPS_RANGE": "ДИАПАЗОН ВРЕМЕННЫХ ШАГОВ",
"UPSCALE_MODEL": "МОДЕЛЬ УВЕЛИЧЕНИЯ",
"VAE": "VAE",
"WEBCAM": "ВЕБ-КАМЕРА"
},
"desktopMenu": {
"confirmQuit": "Открыты несохраненные рабочие процессы; все несохраненные изменения будут потеряны. Проигнорировать это и выйти?",
"confirmReinstall": "Это очистит ваш файл extra_models_config.yaml и начнёт установку заново. Вы уверены?",
"quit": "Выйти",
"confirmReinstall": "Это очистит ваш файл extra_models_config.yaml и начнет установку заново. Вы уверены?",
"reinstall": "Переустановить"
},
"downloadGit": {
"gitWebsite": "Скачать git",
"instructions": "Пожалуйста, скачайте и установите последнюю версию для вашей операционной системы. Кнопка «Скачать git» ниже открывает страницу загрузок git-scm.com.",
"instructions": "Пожалуйста, скачайте и установите последнюю версию для вашей операционной системы. Кнопка 'Скачать git' ниже открывает страницу загрузок git-scm.com.",
"message": "Не удалось найти git. Рабочая копия git необходима для нормальной работы.",
"skip": "Пропустить",
"title": "Скачать git",
"warning": "Если вы уверены, что вам не нужно устанавливать git, или произошла ошибка, вы можете нажать «Пропустить», чтобы обойти эту проверку. Попытка запустить ComfyUI без рабочей копии git в настоящее время не поддерживается."
"warning": "Если вы уверены, что вам не нужно устанавливать git, или произошла ошибка, вы можете нажать 'Пропустить', чтобы обойти эту проверку. Попытка запустить ComfyUI без рабочей копии git в настоящее время не поддерживается."
},
"electronFileDownload": {
"cancel": "Отменить загрузку",
@@ -79,7 +77,7 @@
"customize": "Настроить",
"customizeFolder": "Настроить папку",
"delete": "Удалить",
"deprecated": "Устарело",
"deprecated": "УСТАРЕЛО",
"devices": "Устройства",
"disableAll": "Отключить все",
"download": "Скачать",
@@ -88,11 +86,10 @@
"error": "Ошибка",
"experimental": "БЕТА",
"export": "Экспорт",
"extensionName": "Название расширения",
"feedback": "Обратная связь",
"extensionName": "Имя расширения",
"findIssues": "Найти проблемы",
"firstTimeUIMessage": "Вы впервые используете новый интерфейс. Выберите \"Меню > Использовать новое меню > Отключено\", чтобы восстановить старый интерфейс.",
"goToNode": "Перейти к ноде",
"goToNode": "Перейти к узлу",
"icon": "Иконка",
"imageFailedToLoad": "Не удалось загрузить изображение",
"import": "Импорт",
@@ -102,7 +99,7 @@
"loadAllFolders": "Загрузить все папки",
"loadWorkflow": "Загрузить рабочий процесс",
"loading": "Загрузка",
"logs": "Логи",
"logs": "Журналы",
"newFolder": "Новая папка",
"next": "Далее",
"no": "Нет",
@@ -118,21 +115,21 @@
"refresh": "Обновить",
"reloadToApplyChanges": "Перезагрузите, чтобы применить изменения",
"rename": "Переименовать",
"reportIssue": "Отправить отчёт",
"reportIssueTooltip": "Отправить отчёт об ошибке в Comfy Org",
"reportSent": "Отчёт отправлен",
"reportIssue": "Отправить отчет",
"reportIssueTooltip": "Отправить отчет об ошибке в Comfy Org",
"reportSent": "Отчет отправлен",
"reset": "Сбросить",
"resetKeybindingsTooltip": "Сбросить сочетания клавиш по умолчанию",
"resetKeybindingsTooltip": "Сбросить сочетания клавиш к умолчанию",
"save": "Сохранить",
"searchExtensions": "Поиск расширений",
"searchFailedMessage": "Мы не смогли найти настройки, соответствующие вашему запросу. Попробуйте изменить поисковые термины.",
"searchKeybindings": "Поиск сочетаний клавиш",
"searchModels": "Поиск моделей",
"searchNodes": "Поиск нод",
"searchNodes": "Поиск узлов",
"searchSettings": "Поиск настроек",
"searchWorkflows": "Поиск рабочих процессов",
"settings": "Настройки",
"showReport": "Показать отчёт",
"showReport": "Показать отчет",
"success": "Успех",
"systemInfo": "Информация о системе",
"terminal": "Терминал",
@@ -150,8 +147,8 @@
"zoomOut": "Уменьшить"
},
"groupNode": {
"create": "Создать ноду группы",
"enterName": "Введите название"
"create": "Создать узел группы",
"enterName": "Введите имя"
},
"icon": {
"bookmark": "Закладка",
@@ -169,17 +166,17 @@
"appPathLocationTooltip": "Директория активов приложения ComfyUI. Хранит код и активы ComfyUI",
"cannotWrite": "Невозможно записать в выбранный путь",
"chooseInstallationLocation": "Выберите место установки",
"customNodes": "Пользовательские ноды",
"customNodesDescription": "Переустановите пользовательские ноды из существующих установок ComfyUI.",
"desktopAppSettings": "Настройки десктопного приложения",
"desktopAppSettingsDescription": "Настройте, как ComfyUI ведёт себя на вашем рабочем столе. Вы можете изменить эти настройки позже.",
"customNodes": "Пользовательские узлы",
"customNodesDescription": "Переустановите пользовательские узлы из существующих установок ComfyUI.",
"desktopAppSettings": "Настройки настольного приложения",
"desktopAppSettingsDescription": "Настройте, как ComfyUI ведет себя на вашем рабочем столе. Вы можете изменить эти настройки позже.",
"desktopSettings": "Настройки рабочего стола",
"failedToSelectDirectory": "Не удалось выбрать директорию",
"gpu": "GPU",
"gpuSelection": {
"cpuMode": "Режим CPU",
"cpuModeDescription": "Режим CPU предназначен только для разработчиков и крайне редких случаев.",
"cpuModeDescription2": "Если вы не полностью уверены, что вам это нужно, пожалуйста, проигнорируйте эту галочку и выберите ваш GPU выше.",
"cpuModeDescription": "Режим CPU предназначен только для разработчиков и редких крайних случаев.",
"cpuModeDescription2": "Если вы не абсолютно уверены, что вам это нужно, пожалуйста, проигнорируйте эту галочку и выберите ваш GPU выше.",
"customComfyNeedsPython": "ComfyUI не будет работать, пока python не будет настроен",
"customInstallRequirements": "Установите все требования и зависимости (например, custom torch)",
"customManualVenv": "Вручную настроить python venv",
@@ -194,8 +191,8 @@
"helpImprove": "Пожалуйста, помогите улучшить ComfyUI",
"installLocation": "Место установки",
"installLocationDescription": "Выберите директорию для пользовательских данных ComfyUI. В выбранном месте будет установлена среда Python. Пожалуйста, убедитесь, что на выбранном диске достаточно места (~15 ГБ).",
"installLocationTooltip": "Директория пользовательских данных ComfyUI. Хранит:\n- Среда Python\n- Модели\n- Пользовательские ноды\n",
"insufficientFreeSpace": "Недостаточно места минимально необходимое свободное место",
"installLocationTooltip": "Директория пользовательских данных ComfyUI. Хранит:\n- Среда Python\n- Модели\n- Пользовательские узлы\n",
"insufficientFreeSpace": "Недостаточно места - минимально необходимое свободное место",
"manualConfiguration": {
"createVenv": "Вам потребуется создать виртуальное окружение в следующем каталоге",
"requirements": "Требования",
@@ -208,10 +205,10 @@
"migrateFromExistingInstallation": "Миграция из существующей установки",
"migration": "Миграция",
"migrationOptional": "Миграция является необязательной. Если у вас нет существующей установки, вы можете пропустить этот шаг.",
"migrationSourcePathDescription": "Если у вас уже есть установленный ComfyUI, мы можем скопировать/связать ваши существующие пользовательские файлы и модели с новой установкой. Ваша существующая установка ComfyUI не будет затронута.",
"migrationSourcePathDescription": "Если у вас уже есть установка ComfyUI, мы можем скопировать/связать ваши существующие пользовательские файлы и модели с новой установкой. Ваша существующая установка ComfyUI не будет затронута.",
"moreInfo": "Для получения дополнительной информации, пожалуйста, прочтите нашу",
"parentMissing": "Путь не существует сначала создайте родительский каталог",
"pathExists": "Директория уже существует пожалуйста, убедитесь, что вы сделали резервное копирование всех данных",
"parentMissing": "Путь не существует - сначала создайте родительский каталог",
"pathExists": "Директория уже существует - пожалуйста, убедитесь, что вы сделали резервное копирование всех данных",
"pathValidationFailed": "Не удалось проверить путь",
"privacyPolicy": "политику конфиденциальности",
"selectItemsToMigrate": "Выберите элементы для миграции",
@@ -227,7 +224,7 @@
"userJourneyEvents": "События пользовательского пути"
},
"doNotCollect": {
"customNodeConfigurations": "Пользовательские конфигурации нод",
"customNodeConfigurations": "Пользовательские конфигурации узлов",
"fileSystemInformation": "Информация о файловой системе",
"personalInformation": "Личная информация",
"workflowContents": "Содержание рабочего процесса"
@@ -243,39 +240,17 @@
},
"systemLocations": "Системные места",
"unhandledError": "Неизвестная ошибка",
"updateConsent": "Вы ранее согласились на отчётность об ошибках. Теперь мы отслеживаем метрики событий, чтобы помочь выявить ошибки и улучшить приложение. Личная идентифицируемая информация не собирается."
"updateConsent": "Вы ранее согласились на отчетность об ошибках. Теперь мы отслеживаем событийные метрики, чтобы помочь выявить ошибки и улучшить приложение. Личная идентифицируемая информация не собирается."
},
"issueReport": {
"contactFollowUp": "Свяжитесь со мной для уточнения",
"feedbackTitle": "Помогите нам улучшить ComfyUI, оставив отзыв",
"helpFix": "Помочь исправить это",
"notifyResolve": "Уведомить меня, когда проблема будет решена",
"provideAdditionalDetails": "Предоставьте дополнительные сведения (необязательно)",
"provideEmail": "Укажите вашу электронную почту (необязательно)",
"rating": "Рейтинг",
"stackTrace": "Трассировка стека",
"submitErrorReport": "Отправить отчёт об ошибке (необязательно)",
"systemStats": "Статистика системы",
"validation": {
"invalidEmail": "Пожалуйста, введите действительный адрес электронной почты",
"maxLength": "Сообщение слишком длинное"
}
},
"maintenance": {
"None": "Нет",
"OK": "OK",
"Skipped": "Пропущено",
"allOk": "Проблем не обнаружено.",
"confirmTitle": "Вы уверены?",
"detected": "Обнаружено",
"error": {
"defaultDescription": "Произошла ошибка при выполнении задачи по обслуживанию.",
"taskFailed": "Не удалось выполнить задачу.",
"toastTitle": "Ошибка задачи"
},
"refreshing": "Обновление",
"showManual": "Показать задачи по обслуживанию",
"status": "Статус"
"submitErrorReport": "Отправить отчет об ошибке (необязательно)",
"systemStats": "Статистика системы"
},
"menu": {
"autoQueue": "Автоочередь",
@@ -295,7 +270,7 @@
"queue": "Выполнить",
"queueWorkflow": "Очередь рабочего процесса (Shift для вставки спереди)",
"queueWorkflowFront": "Очередь рабочего процесса (Вставка спереди)",
"refresh": "Обновить определения нод",
"refresh": "Обновить определения узлов",
"resetView": "Сбросить вид холста",
"showMenu": "Показать меню",
"toggleBottomPanel": "Переключить нижнюю панель"
@@ -303,37 +278,36 @@
"menuLabels": {
"About ComfyUI": "О ComfyUI",
"Browse Templates": "Просмотреть шаблоны",
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные ноды",
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные узлы",
"Canvas Toggle Link Visibility": "Переключение видимости ссылки на холст",
"Canvas Toggle Lock": "Переключение блокировки холста",
"Clear Pending Tasks": "Очистить ожидающие задачи",
"Clear Workflow": "Очистить рабочий процесс",
"Clipspace": "Клиппространство",
"Close Current Workflow": "Закрыть текущий рабочий процесс",
"Collapse/Expand Selected Nodes": "Свернуть/развернуть выбранные ноды",
"Collapse/Expand Selected Nodes": "Свернуть/развернуть выбранные узлы",
"Comfy-Org Discord": "Discord Comfy-Org",
"ComfyUI Docs": "Документация ComfyUI",
"ComfyUI Forum": "Форум ComfyUI",
"ComfyUI Issues": "Проблемы ComfyUI",
"Convert selected nodes to group node": "Преобразовать выбранные ноды в групповую ноду",
"Convert selected nodes to group node": "Преобразовать выбранные узлы в групповой узел",
"Desktop User Guide": "Руководство пользователя для настольных ПК",
"Duplicate Current Workflow": "Дублировать текущий рабочий процесс",
"Edit": "Редактировать",
"Export": "Экспортировать",
"Export (API)": "Экспорт (API)",
"Feedback": "Обратная связь",
"Fit Group To Contents": "Подогнать группу под содержимое",
"Fit view to selected nodes": "Подогнать вид под выбранные ноды",
"Give Feedback": "Оставить отзыв",
"Group Selected Nodes": "Сгруппировать выбранные ноды",
"Fit view to selected nodes": "Подогнать вид под выбранные узлы",
"Group Selected Nodes": "Сгруппировать выбранные узлы",
"Help": "Помощь",
"Interrupt": "Прервать",
"Load Default Workflow": "Загрузить стандартный рабочий процесс",
"Manage group nodes": "Управление групповыми нодами",
"Mute/Unmute Selected Nodes": "Отключить/включить звук для выбранных нод",
"Manage group nodes": "Управление групповыми узлами",
"Mute/Unmute Selected Nodes": "Отключить/включить звук для выбранных узлов",
"New": "Новый",
"Next Opened Workflow": "Следующий открытый рабочий процесс",
"Open": "Открыть",
"Open Custom Nodes Folder": "Открыть папку пользовательских нод",
"Open Custom Nodes Folder": "Открыть папку пользовательских узлов",
"Open DevTools": "Открыть инструменты разработчика",
"Open Inputs Folder": "Открыть папку входных данных",
"Open Logs Folder": "Открыть папку журналов",
@@ -341,13 +315,12 @@
"Open Outputs Folder": "Открыть папку выходных данных",
"Open extra_model_paths_yaml": "Открыть extra_model_paths.yaml",
"Pin/Unpin Selected Items": "Закрепить/открепить выбранные элементы",
"Pin/Unpin Selected Nodes": "Закрепить/открепить выбранные ноды",
"Pin/Unpin Selected Nodes": "Закрепить/открепить выбранные узлы",
"Previous Opened Workflow": "Предыдущий открытый рабочий процесс",
"Queue Prompt": "Запрос в очереди",
"Queue Prompt (Front)": "Запрос в очереди (спереди)",
"Quit": "Выйти",
"Redo": "Повторить",
"Refresh Node Definitions": "Обновить определения нод",
"Refresh Node Definitions": "Обновить определения узлов",
"Reinstall": "Переустановить",
"Reset View": "Сбросить вид",
"Restart": "Перезапустить",
@@ -358,23 +331,18 @@
"Toggle Focus Mode": "Переключить режим фокуса",
"Toggle Logs Bottom Panel": "Переключение нижней панели журналов",
"Toggle Model Library Sidebar": "Переключение боковой панели библиотеки моделей",
"Toggle Node Library Sidebar": "Переключение боковой панели библиотеки нод",
"Toggle Node Library Sidebar": "Переключение боковой панели библиотеки узлов",
"Toggle Queue Sidebar": "Переключение боковой панели очереди",
"Toggle Search Box": "Переключить поисковую панель",
"Toggle Terminal Bottom Panel": "Переключение нижней панели терминала",
"Toggle Theme (Dark/Light)": "Переключение темы (Тёмная/Светлая)",
"Toggle Theme (Dark/Light)": "Переключение темы (Темная/Светлая)",
"Toggle Workflows Sidebar": "Переключение боковой панели рабочих процессов",
"Undo": "Отменить",
"Ungroup selected group nodes": "Разгруппировать выбранные групповые ноды",
"Ungroup selected group nodes": "Разгруппировать выбранные групповые узлы",
"Workflow": "Рабочий процесс",
"Zoom In": "Увеличить",
"Zoom Out": "Уменьшить"
},
"missingModelsDialog": {
"doNotAskAgain": "Больше не показывать это",
"missingModels": "Отсутствующие модели",
"missingModelsMessage": "При загрузке графа следующие модели не были найдены"
},
"nodeCategories": {
"3d": "3d",
"3d_models": "3d_модели",
@@ -386,24 +354,24 @@
"attention_experiments": "эксперименты_внимания",
"audio": "аудио",
"batch": "пакет",
"clip": "clip",
"clip": "клип",
"combine": "объединить",
"compositing": "композиционирование",
"compositing": "композитинг",
"cond pair": "условие_пара",
"cond single": "условие_одиночное",
"conditioning": "условие",
"controlnet": "controlnet",
"controlnet": "контрольная_сеть",
"create": "создать",
"custom_sampling": "пользовательский_семплинг",
"custom_sampling": "пользовательская_выборка",
"deprecated": "устаревший",
"flux": "flux",
"gligen": "gligen",
"gligen": "глиген",
"guidance": "направление",
"guiders": "направляющие",
"hooks": "хуки",
"image": "изображение",
"inpaint": "восстановление",
"instructpix2pix": "instructpix2pix",
"instructpix2pix": "инструктпикс2пикс",
"latent": "латентный",
"loaders": "загрузчики",
"ltxv": "ltxv",
@@ -419,26 +387,26 @@
"preprocessors": "предобработчики",
"samplers": "семплеры",
"sampling": "выборка",
"schedulers": "schedulers",
"scheduling": "scheduling",
"schedulers": "планировщики",
"scheduling": "планирование",
"sd3": "sd3",
"sigmas": "сигмы",
"stable_cascade": "стабильная_каскадная",
"style_model": одель_стиля",
"transform": "преобразование",
"unet": "unet",
"upscale_diffusion": "диффузии_апскейла",
"upscaling": "апскейл",
"unet": "унет",
"upscale_diffusion": "увеличение_диффузии",
"upscaling": "увеличение",
"video": "видео",
"video_models": "видеомодели"
},
"nodeTemplates": {
"enterName": "Введите название",
"enterName": "Введите имя",
"saveAsTemplate": "Сохранить как шаблон"
},
"notSupported": {
"continue": "Продолжить",
"continueTooltip": "Я уверен, что моё устройство поддерживается",
"continueTooltip": "Я уверен, что мое устройство поддерживается",
"learnMore": "Узнать больше",
"message": "Поддерживаются только следующие устройства:",
"reportIssue": "Сообщить о проблеме",
@@ -469,14 +437,14 @@
"name": "Использовать классическую систему кэширования"
},
"cache-lru": {
"name": "Использовать LRU кэширование с максимальным количеством N кэшированных результатов нод.",
"name": "Использовать LRU кэширование с максимальным количеством N кэшированных результатов узлов.",
"tooltip": "Может использовать больше ОЗУ/ВРП."
},
"cpu-vae": {
"name": "Запуск VAE на CPU"
},
"cross-attention-method": {
"name": "Метод перекрёстного внимания"
"name": "Метод перекрестного внимания"
},
"cuda-device": {
"name": "Индекс устройства CUDA для использования"
@@ -495,7 +463,7 @@
"name": "Индекс устройства DirectML"
},
"disable-all-custom-nodes": {
"name": "Отключить загрузку всех пользовательских нод."
"name": "Отключить загрузку всех пользовательских узлов."
},
"disable-ipex-optimize": {
"name": "Отключить оптимизацию IPEX"
@@ -511,7 +479,7 @@
"name": "Отключить оптимизацию xFormers"
},
"dont-print-server": {
"name": "Не показывать вывод сервера в консоль."
"name": "Не выводить вывод сервера в консоль."
},
"dont-upcast-attention": {
"name": "Предотвратить повышение внимания"
@@ -558,7 +526,7 @@
},
"reserve-vram": {
"name": "Резервируемая VRAM (ГБ)",
"tooltip": "Установите количество VRAM в ГБ, которое вы хотите зарезервировать для использования вашей ОС/другими программами. По умолчанию резервируется определённое количество в зависимости от вашей ОС."
"tooltip": "Установите количество VRAM в ГБ, которое вы хотите зарезервировать для использования вашей ОС/другими программами. По умолчанию резервируется определенное количество в зависимости от вашей ОС."
},
"text-encoder-precision": {
"name": "Точность текстового кодировщика",
@@ -586,10 +554,10 @@
"openLogs": "Открыть логи",
"process": {
"error": "Не удалось запустить ComfyUI Desktop",
"initial-state": "Загрузка",
"python-setup": "Настройка окружения Python",
"ready": "Завершение",
"starting-server": "Запуск сервера ComfyUI"
"initial-state": "Загрузка...",
"python-setup": "Настройка окружения Python...",
"ready": "Завершение...",
"starting-server": "Запуск сервера ComfyUI..."
},
"reinstall": "Переустановить",
"reportIssue": "Сообщить о проблеме",
@@ -602,8 +570,7 @@
"Canvas": "Холст",
"ColorPalette": "Цветовая палитра",
"Comfy": "Comfy",
"Comfy-Desktop": "Десктопный Comfy",
"ContextMenu": "Контекстное меню",
"Comfy-Desktop": "Comfy рабочий стол",
"CustomColorPalettes": "Пользовательские цветовые палитры",
"DevMode": "Режим разработчика",
"EditTokenWeight": "Редактировать вес токена",
@@ -620,11 +587,11 @@
"Menu": "Меню",
"ModelLibrary": "Библиотека моделей",
"NewEditor": "Новый редактор",
"Node": "Нода",
"Node Search Box": "Поисковая строка нод",
"Node Widget": "Виджет ноды",
"NodeInputConversionSubmenus": "Подменю преобразования ввода ноды",
"NodeLibrary": "Библиотека нод",
"Node": "Узел",
"Node Search Box": "Поисковая строка узлов",
"Node Widget": "Виджет узла",
"NodeInputConversionSubmenus": "Подменю преобразования ввода узла",
"NodeLibrary": "Библиотека узлов",
"Pointer": "Указатель",
"Queue": "Очередь",
"QueueButton": "Кнопка очереди",
@@ -644,7 +611,7 @@
"logout": "Выйти",
"modelLibrary": "Библиотека моделей",
"newBlankWorkflow": "Создайте новый пустой рабочий процесс",
"nodeLibrary": "Библиотека нод",
"nodeLibrary": "Библиотека узлов",
"nodeLibraryTab": {
"sortOrder": "Порядок сортировки"
},
@@ -670,7 +637,7 @@
"confirmOverwriteTitle": "Перезаписать существующий файл?",
"deleteFailed": "Попытка удалить рабочий процесс не удалась.",
"deleteFailedTitle": "Не удалось удалить",
"deleted": "Рабочий процесс удалён",
"deleted": "Рабочий процесс удален",
"dirtyClose": "Файлы ниже были изменены. Вы хотите сохранить их перед закрытием?",
"dirtyCloseTitle": "Сохранить изменения?",
"workflowTreeType": {
@@ -692,12 +659,12 @@
},
"templateWorkflows": {
"template": {
"default": "Генерация изображений",
"default": "Image Generation",
"flux_schnell": "Flux Schnell",
"image2image": "Изображение в изображение",
"upscale": "2-этапный апскейл"
"image2image": "Image to Image",
"upscale": "2 Pass Upscale"
},
"title": "Начните с шаблона"
"title": "Начните работу с шаблона"
},
"userSelect": {
"enterUsername": "Введите имя пользователя",
@@ -711,7 +678,7 @@
"title": "Добро пожаловать в ComfyUI"
},
"workflowService": {
"enterFilename": "Введите название файла",
"enterFilename": "Введите имя файла",
"exportWorkflow": "Экспорт рабочего процесса",
"saveWorkflow": "Сохранить рабочий процесс"
}

File diff suppressed because it is too large Load Diff

View File

@@ -20,14 +20,14 @@
"name": "Включить обрезку элементов DOM (включение может снизить производительность)"
},
"Comfy_DevMode": {
"name": "Включить параметры режима разработчика (сохранение API и т. д.)"
"name": "Включить параметры режима разработчика (сохранение API и т.д.)"
},
"Comfy_DisableFloatRounding": {
"name": "Отключить округление по умолчанию для плавающих виджетов.",
"tooltip": "(требуется перезагрузка страницы) Невозможно отключить округление, если оно установлено узлом на сервере."
},
"Comfy_DisableSliders": {
"name": "Отключить ползунки виджетов нод"
"name": "Отключить ползунки виджетов узлов"
},
"Comfy_EditAttention_Delta": {
"name": "Точность Ctrl+вверх/вниз"
@@ -49,7 +49,7 @@
"name": "Показать меню холста графа"
},
"Comfy_Graph_CtrlShiftZoom": {
"name": "Включить быстрый зум с помощью сочетания клавиш (Ctrl + Shift + Колёсико мыши)"
"name": "Включить быстрый зум с помощью сочетания клавиш (Ctrl + Shift + Перетаскивание)"
},
"Comfy_Graph_LinkMarkers": {
"name": "Маркер середины ссылки",
@@ -63,10 +63,10 @@
"name": "Скорость зума холста"
},
"Comfy_GroupSelectedNodes_Padding": {
"name": "Отступ для выбранных нод группы"
"name": "Отступ для выбранных узлов группы"
},
"Comfy_Group_DoubleClickTitleToEdit": {
"name": "Дважды щёлкните по заголовку группы, чтобы редактировать"
"name": "Дважды щелкните по заголовку группы, чтобы редактировать"
},
"Comfy_LinkRelease_Action": {
"name": "Действие при отпускании ссылки (без модификатора)",
@@ -98,11 +98,11 @@
},
"Comfy_MaskEditor_BrushAdjustmentSpeed": {
"name": "Множитель скорости регулировки кисти",
"tooltip": "Управляет тем, как быстро изменяются размер и жёсткость кисти при регулировке. Более высокие значения означают более быстрые изменения."
"tooltip": "Управляет тем, как быстро изменяются размер и жесткость кисти при регулировке. Более высокие значения означают более быстрые изменения."
},
"Comfy_MaskEditor_UseDominantAxis": {
"name": "Закрепить регулировку кисти по доминирующей оси",
"tooltip": "При включении регулировки кисти будет влиять только на размер или жёсткость в зависимости от того, в каком направлении вы двигаетесь больше"
"tooltip": "При включении регулировки кисти будет влиять только на размер ИЛИ жесткость в зависимости от того, в каком направлении вы двигаетесь больше"
},
"Comfy_MaskEditor_UseNewEditor": {
"name": "Использовать новый редактор масок",
@@ -113,29 +113,29 @@
"tooltip": "Если true, все папки будут загружены, как только вы откроете библиотеку моделей (это может вызвать задержки при загрузке). Если false, корневые папки моделей будут загружены только после нажатия на них."
},
"Comfy_ModelLibrary_NameFormat": {
"name": "Какое название отображать в древовидном представлении библиотеки моделей",
"name": "Какое имя отображать в древовидном представлении библиотеки моделей",
"options": {
"filename": "название файла",
"filename": "имя файла",
"title": "название"
},
"tooltip": "Выберите \"название файла\", чтобы отобразить упрощённый вид сырого названия файла (без директории или расширения \".safetensors\") в списке моделей. Выберите \"название\", чтобы отобразить настраиваемое название метаданных модели."
"tooltip": "Выберите \"имя файла\", чтобы отобразить упрощенный вид сырого имени файла (без директории или расширения \".safetensors\") в списке моделей. Выберите \"название\", чтобы отобразить настраиваемое название метаданных модели."
},
"Comfy_NodeBadge_NodeIdBadgeMode": {
"name": "Режим значка ID ноды",
"name": "Режим значка ID узла",
"options": {
"None": "Нет",
"Show all": "Показать все"
}
},
"Comfy_NodeBadge_NodeLifeCycleBadgeMode": {
"name": "Режим значка жизненного цикла ноды",
"name": "Режим значка жизненного цикла узла",
"options": {
"None": "Нет",
"Show all": "Показать все"
}
},
"Comfy_NodeBadge_NodeSourceBadgeMode": {
"name": "Режим значка источника ноды",
"name": "Режим значка источника узла",
"options": {
"Hide built-in": "Скрыть встроенные",
"None": "Нет",
@@ -143,63 +143,63 @@
}
},
"Comfy_NodeInputConversionSubmenus": {
"name": "В контекстном меню ноды разместите элементы, которые конвертируют между вводом/виджетом в подменю."
"name": "В контекстном меню узла разместите элементы, которые конвертируют между вводом/виджетом в подменю."
},
"Comfy_NodeSearchBoxImpl": {
"name": "Реализация поискового поля нод",
"name": "Реализация поискового поля узлов",
"options": {
"default": "по умолчанию",
"litegraph (legacy)": "litegraph (устаревший)"
}
},
"Comfy_NodeSearchBoxImpl_NodePreview": {
"name": "Предварительный просмотр ноды",
"name": "Предварительный просмотр узла",
"tooltip": "Применяется только к стандартной реализации"
},
"Comfy_NodeSearchBoxImpl_ShowCategory": {
"name": "Показать категорию ноды в результатах поиска",
"name": "Показать категорию узла в результатах поиска",
"tooltip": "Применяется только к стандартной реализации"
},
"Comfy_NodeSearchBoxImpl_ShowIdName": {
"name": "Показать название ID ноды в результатах поиска",
"name": "Показать имя ID узла в результатах поиска",
"tooltip": "Применяется только к стандартной реализации"
},
"Comfy_NodeSearchBoxImpl_ShowNodeFrequency": {
"name": "Показать частоту ноды в результатах поиска",
"name": "Показать частоту узла в результатах поиска",
"tooltip": "Применяется только к стандартной реализации"
},
"Comfy_NodeSuggestions_number": {
"name": "Количество предложенных нод",
"name": "Количество предложений узлов",
"tooltip": "Только для поля поиска litegraph/контекстного меню"
},
"Comfy_Node_AutoSnapLinkToSlot": {
"name": "Автоматически привязывать ссылку к слоту ноды",
"tooltip": "При перетаскивании ссылки над нодой ссылка автоматически привязывается к подходящему входному слоту ноды"
"name": "Автоматически привязывать ссылку к слоту узла",
"tooltip": "При перетаскивании ссылки над узлом ссылка автоматически привязывается к подходящему входному слоту узла"
},
"Comfy_Node_BypassAllLinksOnDelete": {
"name": "Сохранить все ссылки при удалении нод",
"tooltip": "При удалении ноды попытаться переподключить все её входные и выходные ссылки (обходя удалённую ноду)"
"name": "Сохранить все ссылки при удалении узлов",
"tooltip": "При удалении узла попытаться переподключить все его входные и выходные ссылки (обходя удаленный узел)"
},
"Comfy_Node_DoubleClickTitleToEdit": {
"name": "Дважды щёлкните по заголовку ноды, чтобы редактировать"
"name": "Дважды щелкните по заголовку узла, чтобы редактировать"
},
"Comfy_Node_MiddleClickRerouteNode": {
"name": "Средний щелчок создаёт новую ноду перенаправления"
"name": "Средний щелчок создает новый узел перенаправления"
},
"Comfy_Node_Opacity": {
"name": "Непрозрачность ноды"
"name": "Непрозрачность узла"
},
"Comfy_Node_ShowDeprecated": {
"name": "Показать устаревшие ноды в поиске",
"tooltip": "Устаревшие ноды по умолчанию скрыты в интерфейсе, но остаются функциональными в существующих рабочих процессах, которые их используют."
"name": "Показать устаревшие узлы в поиске",
"tooltip": "Устаревшие узлы по умолчанию скрыты в интерфейсе, но остаются функциональными в существующих рабочих процессах, которые их используют."
},
"Comfy_Node_ShowExperimental": {
"name": "Показать экспериментальные ноды в поиске",
"tooltip": "Экспериментальные ноды помечены как таковые в интерфейсе и могут подвергаться значительным изменениям или удалению в будущих версиях. Используйте с осторожностью в производственных рабочих процессах"
"name": "Показать экспериментальные узлы в поиске",
"tooltip": "Экспериментальные узлы помечены как таковые в интерфейсе и могут подвергаться значительным изменениям или удалению в будущих версиях. Используйте с осторожностью в производственных рабочих процессах"
},
"Comfy_Node_SnapHighlightsNode": {
"name": "Подсветка ноды при привязке",
"tooltip": "При перетаскивании ссылки над нодой с подходящим входным слотом, нода подсвечивается"
"name": "Подсветка узла при привязке",
"tooltip": "При перетаскивании ссылки над узлом с подходящим входным слотом, узел подсвечивается"
},
"Comfy_Pointer_ClickBufferTime": {
"name": "Задержка дрейфа щелчка указателя",
@@ -218,7 +218,7 @@
"tooltip": "При отображении предварительного просмотра в виджете изображения, преобразуйте его в легковесное изображение, например, webp, jpeg, webp;50 и т.д."
},
"Comfy_PromptFilename": {
"name": "Запрос названия файла при сохранении рабочего процесса"
"name": "Запрос имени файла при сохранении рабочего процесса"
},
"Comfy_QueueButton_BatchCountLimit": {
"name": "Ограничение количества партий",
@@ -230,7 +230,7 @@
},
"Comfy_RerouteBeta": {
"name": "Участвовать в бета-тестировании перенаправления",
"tooltip": "Включает новые нативные перенаправления.\n\nПеренаправления можно добавлять, удерживая alt и перетаскивая от линии ссылки или в меню ссылки.\n\nОтключение этой опции не разрушительно перенаправления скрыты."
"tooltip": "Включает новые нативные перенаправления.\n\nПеренаправления можно добавлять, удерживая alt и перетаскивая от линии ссылки или в меню ссылки.\n\nОтключение этой опции не разрушительно - перенаправления скрыты."
},
"Comfy_Sidebar_Location": {
"name": "Расположение боковой панели",
@@ -248,7 +248,7 @@
},
"Comfy_SnapToGrid_GridSize": {
"name": "Размер сетки привязки",
"tooltip": "При перетаскивании и изменении размера нод, удерживая shift, они будут выровнены по сетке, это контролирует размер этой сетки."
"tooltip": "При перетаскивании и изменении размера узлов, удерживая shift, они будут выровнены по сетке, это контролирует размер этой сетки."
},
"Comfy_TextareaWidget_FontSize": {
"name": "Размер шрифта виджета текстовой области"
@@ -268,8 +268,8 @@
}
},
"Comfy_Validation_NodeDefs": {
"name": "Проверка определений нод (медленно)",
"tooltip": "Рекомендуется для разработчиков нод. Это проверит все определения нод при запуске."
"name": "Проверка определений узлов (медленно)",
"tooltip": "Рекомендуется для разработчиков узлов. Это проверит все определения узлов при запуске."
},
"Comfy_Validation_Workflows": {
"name": "Проверка рабочих процессов"
@@ -292,10 +292,10 @@
"name": "Показать предупреждение об отсутствующих моделях"
},
"Comfy_Workflow_ShowMissingNodesWarning": {
"name": "Показать предупреждение об отсутствующих нодах"
"name": "Показать предупреждение об отсутствующих узлах"
},
"Comfy_Workflow_SortNodeIdOnSave": {
"name": "Сортировать ID нод при сохранении рабочего процесса"
"name": "Сортировать ID узлов при сохранении рабочего процесса"
},
"Comfy_Workflow_WorkflowTabsPosition": {
"name": "Положение открытых рабочих процессов",
@@ -307,10 +307,7 @@
},
"LiteGraph_Canvas_MaximumFps": {
"name": "Максимум FPS",
"tooltip": "Максимальное количество кадров в секунду, которое холст может рендерить. Ограничивает использование GPU за счёт плавности. Если 0, используется частота обновления экрана. По умолчанию: 0"
},
"LiteGraph_ContextMenu_Scaling": {
"name": "Масштабирование комбинированных виджетов меню узлов (списков) при увеличении"
"tooltip": "Максимальное количество кадров в секунду, которое холст может рендерить. Ограничивает использование GPU за счет плавности. Если 0, используется частота обновления экрана. По умолчанию: 0"
},
"pysssss_SnapToGrid": {
"name": "Всегда привязываться к сетке"

View File

@@ -20,12 +20,12 @@
"Comfy-Desktop_OpenDevTools": {
"label": "打开开发者工具"
},
"Comfy-Desktop_OpenFeedbackPage": {
"label": "反馈"
},
"Comfy-Desktop_OpenUserGuide": {
"label": "桌面用户指南"
},
"Comfy-Desktop_Quit": {
"label": "退出"
},
"Comfy-Desktop_Reinstall": {
"label": "重新安装"
},
@@ -83,9 +83,6 @@
"Comfy_ExportWorkflowAPI": {
"label": "导出工作流API格式"
},
"Comfy_Feedback": {
"label": "反馈"
},
"Comfy_Graph_FitGroupToContents": {
"label": "适应节点框到内容"
},
@@ -110,9 +107,6 @@
"Comfy_Help_OpenComfyUIDocs": {
"label": "打开ComfyUI文档"
},
"Comfy_Help_OpenComfyUIForum": {
"label": "打开 Comfy-Org 论坛"
},
"Comfy_Help_OpenComfyUIIssues": {
"label": "打开ComfyUI问题"
},

View File

@@ -43,9 +43,7 @@
"WEBCAM": "摄像头"
},
"desktopMenu": {
"confirmQuit": "有未保存的工作流程开启;任何未保存的更改都将丢失。忽略此警告并退出?",
"confirmReinstall": "这将清除您的 extra_models_config.yaml 文件,并重新开始安装。您确定吗?",
"quit": "退出",
"reinstall": "重新安装"
},
"downloadGit": {
@@ -89,7 +87,6 @@
"experimental": "测试版",
"export": "导出",
"extensionName": "扩展名称",
"feedback": "反馈",
"findIssues": "查找问题",
"firstTimeUIMessage": "这是您第一次使用新界面。选择 \"菜单 > 使用新菜单 > 禁用\" 来恢复旧界面。",
"goToNode": "转到节点",
@@ -247,35 +244,13 @@
},
"issueReport": {
"contactFollowUp": "跟进联系我",
"feedbackTitle": "通过提供反馈帮助我们改进ComfyUI",
"helpFix": "帮助修复这个",
"notifyResolve": "解决时通知我",
"provideAdditionalDetails": "提供额外的详细信息(可选)",
"provideEmail": "提供您的电子邮件(可选)",
"rating": "评分",
"stackTrace": "堆栈跟踪",
"submitErrorReport": "提交错误报告(可选)",
"systemStats": "系统状态",
"validation": {
"invalidEmail": "请输入有效的电子邮件地址",
"maxLength": "消息过长"
}
},
"maintenance": {
"None": "无",
"OK": "确定",
"Skipped": "跳过",
"allOk": "未检测到任何问题。",
"confirmTitle": "你确定吗?",
"detected": "检测到",
"error": {
"defaultDescription": "运行维护任务时发生错误。",
"taskFailed": "任务运行失败。",
"toastTitle": "任务错误"
},
"refreshing": "刷新中",
"showManual": "显示维护任务",
"status": "状态"
"systemStats": "系统状态"
},
"menu": {
"autoQueue": "自动执行",
@@ -313,7 +288,6 @@
"Collapse/Expand Selected Nodes": "折叠/展开选定节点",
"Comfy-Org Discord": "Comfy-Org Discord",
"ComfyUI Docs": "ComfyUI 文档",
"ComfyUI Forum": "ComfyUI 论坛",
"ComfyUI Issues": "ComfyUI 问题",
"Convert selected nodes to group node": "将选中节点转换为组节点",
"Desktop User Guide": "桌面端用户指南",
@@ -321,9 +295,9 @@
"Edit": "编辑",
"Export": "导出",
"Export (API)": "导出 (API)",
"Feedback": "反馈",
"Fit Group To Contents": "适应组内容",
"Fit view to selected nodes": "适应视图到选中节点",
"Give Feedback": "提供反馈",
"Group Selected Nodes": "将选中节点转换为组节点",
"Help": "帮助",
"Interrupt": "中断",
@@ -345,7 +319,6 @@
"Previous Opened Workflow": "上一个打开的工作流",
"Queue Prompt": "执行提示词",
"Queue Prompt (Front)": "执行提示词 (优先执行)",
"Quit": "退出",
"Redo": "重做",
"Refresh Node Definitions": "刷新节点定义",
"Reinstall": "重新安装",
@@ -370,11 +343,6 @@
"Zoom In": "放大画面",
"Zoom Out": "缩小画面"
},
"missingModelsDialog": {
"doNotAskAgain": "不再显示此消息",
"missingModels": "缺少模型",
"missingModelsMessage": "加载图表时,未找到以下模型"
},
"nodeCategories": {
"3d": "3d",
"3d_models": "3D模型",
@@ -603,7 +571,6 @@
"ColorPalette": "色彩主题",
"Comfy": "Comfy",
"Comfy-Desktop": "Comfy桌面版",
"ContextMenu": "上下文菜单",
"CustomColorPalettes": "自定义色彩主题",
"DevMode": "开发模式",
"EditTokenWeight": "编辑令牌权重",

View File

@@ -309,9 +309,6 @@
"name": "最大FPS",
"tooltip": "画布允许渲染的最大帧数。限制GPU使用以换取流畅度。如果为0则使用屏幕刷新率。默认值0"
},
"LiteGraph_ContextMenu_Scaling": {
"name": "放大时缩放节点组合部件菜单(列表)"
},
"pysssss_SnapToGrid": {
"name": "始终吸附到网格"
}

View File

@@ -104,12 +104,6 @@ const router = createRouter({
name: 'DesktopStartView',
component: () => import('@/views/DesktopStartView.vue'),
beforeEnter: guardElectronAccess
},
{
path: 'maintenance',
name: 'MaintenanceView',
component: () => import('@/views/MaintenanceView.vue'),
beforeEnter: guardElectronAccess
}
]
}

View File

@@ -877,15 +877,6 @@ export class ComfyApi extends EventTarget {
async getFolderPaths(): Promise<Record<string, string[]>> {
return (await axios.get(this.internalURL('/folder_paths'))).data
}
/**
* Gets the custom nodes i18n data from the server.
*
* @returns The custom nodes i18n data
*/
async getCustomNodesI18n(): Promise<Record<string, any>> {
return (await axios.get(this.apiURL('/i18n'))).data
}
}
export const api = new ComfyApi()

View File

@@ -942,7 +942,7 @@ export class ComfyApp {
api.addEventListener('execution_error', ({ detail }) => {
this.lastExecutionError = detail
useDialogService().showExecutionErrorDialog({ error: detail })
useDialogService().showExecutionErrorDialog(detail)
this.canvas.draw(true, true)
})
@@ -1037,6 +1037,33 @@ export class ComfyApp {
await useExtensionService().invokeExtensionsAsync('init')
await this.registerNodes()
// Load previous workflow
let restored = false
try {
const loadWorkflow = async (json) => {
if (json) {
const workflow = JSON.parse(json)
const workflowName = getStorageValue('Comfy.PreviousWorkflow')
await this.loadGraphData(workflow, true, true, workflowName)
return true
}
}
const clientId = api.initialClientId ?? api.clientId
restored =
(clientId &&
(await loadWorkflow(
sessionStorage.getItem(`workflow:${clientId}`)
))) ||
(await loadWorkflow(localStorage.getItem('workflow')))
} catch (err) {
console.error('Error loading previous workflow', err)
}
// We failed to restore a workflow so load the default
if (!restored) {
await this.loadGraphData()
}
this.#addDrawNodeHandler()
this.#addDrawGroupsHandler()
this.#addDropHandler()
@@ -1286,19 +1313,17 @@ export class ComfyApp {
graphData.models &&
useSettingStore().get('Comfy.Workflow.ShowMissingModelsWarning')
) {
const modelStore = useModelStore()
for (const m of graphData.models) {
const modelFolder = await modelStore.getLoadedModelFolder(m.directory)
// @ts-expect-error
if (!modelFolder) m.directory_invalid = true
const modelsAvailable = modelFolder?.models
const modelExists =
modelsAvailable &&
Object.values(modelsAvailable).some(
(model) => model.file_name === m.name
)
if (!modelExists) missingModels.push(m)
const models_available = await useModelStore().getLoadedModelFolder(
m.directory
)
if (models_available === null) {
// @ts-expect-error
m.directory_invalid = true
missingModels.push(m)
} else if (!(m.name in models_available.models)) {
missingModels.push(m)
}
}
}

View File

@@ -272,7 +272,7 @@ LGraphCanvas.prototype.computeVisibleNodes = function (): LGraphNode[] {
w.element.hidden = actualHidden
w.element.style.display = actualHidden ? 'none' : null
if (actualHidden && !wasHidden) {
w.options.onHide?.(w as DOMWidget<HTMLElement, object>)
w.options.onHide?.(w)
}
}
}

View File

@@ -1,6 +1,5 @@
import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
import ExecutionErrorDialogContent from '@/components/dialog/content/ExecutionErrorDialogContent.vue'
import IssueReportDialogContent from '@/components/dialog/content/IssueReportDialogContent.vue'
import LoadWorkflowWarning from '@/components/dialog/content/LoadWorkflowWarning.vue'
import MissingModelsWarning from '@/components/dialog/content/MissingModelsWarning.vue'
import PromptDialogContent from '@/components/dialog/content/PromptDialogContent.vue'
@@ -9,6 +8,8 @@ import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.
import TemplateWorkflowsContent from '@/components/templates/TemplateWorkflowsContent.vue'
import { t } from '@/i18n'
import { type ShowDialogOptions, useDialogStore } from '@/stores/dialogStore'
import type { ExecutionErrorWsMessage } from '@/types/apiTypes'
import type { MissingNodeType } from '@/types/comfy'
export type ConfirmationDialogType =
| 'default'
@@ -19,9 +20,10 @@ export type ConfirmationDialogType =
export const useDialogService = () => {
const dialogStore = useDialogStore()
function showLoadWorkflowWarning(
props: InstanceType<typeof LoadWorkflowWarning>['$props']
) {
function showLoadWorkflowWarning(props: {
missingNodeTypes: MissingNodeType[]
[key: string]: any
}) {
dialogStore.showDialog({
key: 'global-load-workflow-warning',
component: LoadWorkflowWarning,
@@ -29,9 +31,11 @@ export const useDialogService = () => {
})
}
function showMissingModelsWarning(
props: InstanceType<typeof MissingModelsWarning>['$props']
) {
function showMissingModelsWarning(props: {
missingModels: any[]
paths: Record<string, string[]>
[key: string]: any
}) {
dialogStore.showDialog({
key: 'global-missing-models-warning',
component: MissingModelsWarning,
@@ -63,34 +67,21 @@ export const useDialogService = () => {
})
}
function showExecutionErrorDialog(
props: InstanceType<typeof ExecutionErrorDialogContent>['$props']
) {
function showExecutionErrorDialog(error: ExecutionErrorWsMessage) {
dialogStore.showDialog({
key: 'global-execution-error',
component: ExecutionErrorDialogContent,
props
props: {
error
}
})
}
function showTemplateWorkflowsDialog(
props: InstanceType<typeof TemplateWorkflowsContent>['$props'] = {}
) {
function showTemplateWorkflowsDialog() {
dialogStore.showDialog({
key: 'global-template-workflows',
title: t('templateWorkflows.title'),
component: TemplateWorkflowsContent,
props
})
}
function showIssueReportDialog(
props: InstanceType<typeof IssueReportDialogContent>['$props']
) {
dialogStore.showDialog({
key: 'global-issue-report',
component: IssueReportDialogContent,
props
component: TemplateWorkflowsContent
})
}
@@ -171,7 +162,6 @@ export const useDialogService = () => {
showAboutDialog,
showExecutionErrorDialog,
showTemplateWorkflowsDialog,
showIssueReportDialog,
prompt,
confirm
}

View File

@@ -367,6 +367,7 @@ export const useLitegraphService = () => {
const w = node.widgets[node.widgets.length - 1]
shiftY = w.last_y
if (w.computeSize) {
// @ts-expect-error requires 1 param
shiftY += w.computeSize()[1] + 4
// @ts-expect-error computedHeight only exists for DOMWidget
} else if (w.computedHeight) {

View File

@@ -2,7 +2,6 @@ import { LGraphCanvas } from '@comfyorg/litegraph'
import { toRaw } from 'vue'
import { t } from '@/i18n'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { blankGraph, defaultGraph } from '@/scripts/defaultGraph'
import { downloadBlob } from '@/scripts/utils'
@@ -122,18 +121,6 @@ export const useWorkflowService = () => {
await app.loadGraphData(defaultGraph)
}
/**
* Load the tutorial workflow
*/
const loadTutorialWorkflow = async () => {
const tutorialWorkflow = await fetch(
api.fileURL('/templates/default.json')
).then((r) => r.json())
await app.loadGraphData(tutorialWorkflow, false, false, 'tutorial', {
showMissingModelsDialog: true
})
}
/**
* Load a blank workflow
*/
@@ -379,7 +366,6 @@ export const useWorkflowService = () => {
saveWorkflow,
loadDefaultWorkflow,
loadBlankWorkflow,
loadTutorialWorkflow,
reloadCurrentWorkflow,
openWorkflow,
closeWorkflow,

View File

@@ -1,175 +0,0 @@
import type { InstallValidation } from '@comfyorg/comfyui-electron-types'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { DESKTOP_MAINTENANCE_TASKS } from '@/constants/desktopMaintenanceTasks'
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
import { electronAPI } from '@/utils/envUtil'
/** State of a maintenance task, managed by the maintenance task store. */
type MaintenanceTaskState = 'warning' | 'error' | 'OK' | 'skipped'
// Type not exported by API
type ValidationState = InstallValidation['basePath']
// Add index to API type
type IndexedUpdate = InstallValidation & Record<string, ValidationState>
/** State of a maintenance task, managed by the maintenance task store. */
export class MaintenanceTaskRunner {
constructor(readonly task: MaintenanceTask) {}
private _state?: MaintenanceTaskState
/** The current state of the task. Setter also controls {@link resolved} as a side-effect. */
get state() {
return this._state
}
/** Updates the task state and {@link resolved} status. */
setState(value: MaintenanceTaskState) {
// Mark resolved
if (this._state === 'error' && value === 'OK') this.resolved = true
// Mark unresolved (if previously resolved)
if (value === 'error') this.resolved &&= false
this._state = value
}
/** `true` if the task has been resolved (was `error`, now `OK`). This is a side-effect of the {@link state} setter. */
resolved?: boolean
/** Whether the task state is currently being refreshed. */
refreshing?: boolean
/** Whether the task is currently running. */
executing?: boolean
/** The error message that occurred when the task failed. */
error?: string
update(update: IndexedUpdate) {
const state = update[this.task.id]
this.refreshing = state === undefined
if (state) this.setState(state)
}
finaliseUpdate(update: IndexedUpdate) {
this.refreshing = false
this.setState(update[this.task.id] ?? 'skipped')
}
/** Wraps the execution of a maintenance task, updating state and rethrowing errors. */
async execute(task: MaintenanceTask) {
try {
this.executing = true
const success = await task.execute()
if (!success) return false
this.error = undefined
return true
} catch (error) {
this.error = (error as Error)?.message
throw error
} finally {
this.executing = false
}
}
}
/**
* User-initiated maintenance tasks. Currently only used by the desktop app maintenance view.
*
* Includes running state, task list, and execution / refresh logic.
* @returns The maintenance task store
*/
export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
/** Refresh should run for at least this long, even if it completes much faster. Ensures refresh feels like it is doing something. */
const electron = electronAPI()
// Reactive state
const isRefreshing = ref(false)
const isRunningTerminalCommand = computed(() =>
tasks.value
.filter((task) => task.usesTerminal)
.some((task) => getRunner(task)?.executing)
)
const isRunningInstallationFix = computed(() =>
tasks.value
.filter((task) => task.isInstallationFix)
.some((task) => getRunner(task)?.executing)
)
// Task list
const tasks = ref(DESKTOP_MAINTENANCE_TASKS)
const taskStates = ref(
new Map<MaintenanceTask['id'], MaintenanceTaskRunner>(
DESKTOP_MAINTENANCE_TASKS.map((x) => [x.id, new MaintenanceTaskRunner(x)])
)
)
/** True if any tasks are in an error state. */
const anyErrors = computed(() =>
tasks.value.some((task) => getRunner(task).state === 'error')
)
/**
* Returns the matching state object for a task.
* @param task Task to get the matching state object for
* @returns The state object for this task
*/
const getRunner = (task: MaintenanceTask) => taskStates.value.get(task.id)!
/**
* Updates the task list with the latest validation state.
* @param validationUpdate Update details passed in by electron
*/
const processUpdate = (validationUpdate: InstallValidation) => {
const update = validationUpdate as IndexedUpdate
isRefreshing.value = true
// Update each task state
for (const task of tasks.value) {
getRunner(task).update(update)
}
// Final update
if (!update.inProgress && isRefreshing.value) {
isRefreshing.value = false
for (const task of tasks.value) {
getRunner(task).finaliseUpdate(update)
}
}
}
/** Clears the resolved status of tasks (when changing filters) */
const clearResolved = () => {
for (const task of tasks.value) {
getRunner(task).resolved &&= false
}
}
/** @todo Refreshes Electron tasks only. */
const refreshDesktopTasks = async () => {
isRefreshing.value = true
console.log('Refreshing desktop tasks')
await electron.Validation.validateInstallation(processUpdate)
}
const execute = async (task: MaintenanceTask) => {
return getRunner(task).execute(task)
}
return {
tasks,
isRefreshing,
isRunningTerminalCommand,
isRunningInstallationFix,
execute,
getRunner,
processUpdate,
clearResolved,
/** True if any tasks are in an error state. */
anyErrors,
refreshDesktopTasks
}
})

View File

View File

@@ -1,50 +0,0 @@
import type { VueSeverity } from '../primeVueTypes'
interface MaintenanceTaskButton {
/** The text to display on the button. */
text?: string
/** CSS classes used for the button icon, e.g. 'pi pi-external-link' */
icon?: string
}
/** A maintenance task, used by the maintenance page. */
export interface MaintenanceTask {
/** ID string used as i18n key */
id: string
/** The display name of the task, e.g. Git */
name: string
/** Short description of the task. */
shortDescription?: string
/** Description of the task when it is in an error state. */
errorDescription?: string
/** Description of the task when it is in a warning state. */
warningDescription?: string
/** Full description of the task when it is in an OK state. */
description?: string
/** URL to the image to show in card mode. */
headerImg?: string
/** The button to display on the task card / list item. */
button?: MaintenanceTaskButton
/** Whether to show a confirmation dialog before running the task. */
requireConfirm?: boolean
/** The text to display in the confirmation dialog. */
confirmText?: string
/** Called by onClick to run the actual task. */
execute: (args?: unknown[]) => boolean | Promise<boolean>
/** Show the button with `severity="danger"` */
severity?: VueSeverity
/** Whether this task should display the terminal window when run. */
usesTerminal?: boolean
/** If `true`, successful completion of this task will refresh install validation and automatically continue if successful. */
isInstallationFix?: boolean
}
/** The filter options for the maintenance task list. */
export interface MaintenanceFilter {
/** CSS classes used for the filter button icon, e.g. 'pi pi-cross' */
icon: string
/** The text to display on the filter button. */
value: string
/** The tasks to display when this filter is selected. */
tasks: ReadonlyArray<MaintenanceTask>
}

View File

@@ -1,5 +1,3 @@
import { z } from 'zod'
export type DefaultField = 'Workflow' | 'Logs' | 'SystemStats' | 'Settings'
export interface ReportField {
@@ -16,7 +14,7 @@ export interface ReportField {
/**
* The data associated with this field, sent as part of the report.
*/
getData: () => unknown
data: Record<string, unknown>
/**
* Indicates whether the field requires explicit opt-in from the user
@@ -24,42 +22,3 @@ export interface ReportField {
*/
optIn: boolean
}
export interface IssueReportPanelProps {
/**
* The type of error being reported. This is used to categorize the error.
*/
errorType: string
/**
* Which of the default fields to include in the report.
*/
defaultFields?: DefaultField[]
/**
* Additional fields to include in the report.
*/
extraFields?: ReportField[]
/**
* Tags that will be added to the report. Tags are used to further categorize the error.
*/
tags?: Record<string, string>
/**
* The title displayed in the dialog.
*/
title?: string
}
const checkboxField = z.boolean().optional()
export const issueReportSchema = z
.object({
contactInfo: z.string().email().max(320).optional().or(z.literal('')),
details: z.string().max(5_000).optional()
})
.catchall(checkboxField)
.refine((data) => Object.values(data).some((value) => value), {
path: ['details']
})
export type IssueReportFormData = z.infer<typeof issueReportSchema>

View File

@@ -1,24 +1,10 @@
import '@comfyorg/litegraph'
import type { LLink } from '@comfyorg/litegraph'
import type { DOMWidget } from '@/scripts/domWidget'
import type { ComfyNodeDef } from '@/types/apiTypes'
import type { NodeId } from './comfyWorkflow'
/** ComfyUI extensions of litegraph */
declare module '@comfyorg/litegraph/dist/types/widgets' {
interface IWidgetOptions {
/** Currently used by DOM widgets only. Declaring here reduces complexity. */
onHide?: (widget: DOMWidget) => void
}
interface IBaseWidget {
onRemove?: () => void
beforeQueued?: () => unknown
}
}
/**
* ComfyUI extensions of litegraph
*/

View File

@@ -1,10 +0,0 @@
/** Button, Tag, etc severity type is 'string' instead of this list. */
export type VueSeverity =
| 'primary'
| 'secondary'
| 'success'
| 'info'
| 'warn'
| 'help'
| 'danger'
| 'contrast'

View File

@@ -8,7 +8,6 @@ export type SettingInputType =
| 'text'
| 'image'
| 'color'
| 'url'
| 'hidden'
export type SettingCustomRenderer = (

Some files were not shown because too many files have changed in this diff Show More