Compare commits

...

62 Commits

Author SHA1 Message Date
Comfy Org PR Bot
cdddf359a8 1.17.7 (#3569)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-22 18:52:13 -04:00
Christian Byrne
8558f87547 [API Node] User management (#3567)
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Chenlei Hu <hcl@comfy.org>
2025-04-22 18:48:45 -04:00
Chenlei Hu
262991db6b [Bug] Prevent node pasting in signin dialog (#3568) 2025-04-22 16:18:25 -04:00
Chenlei Hu
585d52e24e [Bug] Fix DOM widgets position when canvas bounding box change (#3565)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-22 15:11:54 -04:00
Chenlei Hu
b7535755f0 Revert "[nit] Remove tab index on canvas element" (#3566) 2025-04-22 12:58:37 -04:00
Chenlei Hu
6b7b0f6ec1 [nit] Remove tab index on canvas element (#3563) 2025-04-22 11:40:22 -04:00
filtered
c7318bcf0a Update node search to use litegraph LinkConnector (#3546)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-22 10:35:49 -04:00
Christian Byrne
11f909436c [Manager] Fix reactivity of node pack version options dropdown (#3557) 2025-04-22 09:45:46 -04:00
Comfy Org PR Bot
d8f4dc95bb 1.17.6 (#3558)
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-04-21 22:50:43 -07:00
Christian Byrne
c1bc664edd Add command to open signin dialog (#3556)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-21 22:46:31 -07:00
Christian Byrne
e7fe2046ba Checkout on credit add (#3555) 2025-04-21 22:46:05 -07:00
Comfy Org PR Bot
bf4ad38e9b 1.17.5 (#3551)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-21 20:08:30 -04:00
Chenlei Hu
2b024bb186 [Cleanup] Remove LGraphNode.isValidWidgetLink (#3549) 2025-04-21 15:48:55 -04:00
filtered
6e5930c355 [API] Add sockets to custom widgets by default (#3548) 2025-04-21 15:24:48 -04:00
Comfy Org PR Bot
6151d487c6 [chore] Update litegraph to 0.13.8 (#3547)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-04-22 02:17:12 +10:00
Comfy Org PR Bot
e027a9bf44 [chore] Update litegraph to 0.13.7 (#3544)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-04-21 11:00:10 -04:00
filtered
53ee5904e8 [TS] Fix serialisation type (#3541) 2025-04-21 21:13:42 +10:00
Christian Byrne
f82bb71b1e Fix client => canvas position conversion (#3540) 2025-04-21 20:21:25 +10:00
Terry Jia
40d08a890d [3d] move default values of backgroundColor, LightIntensity, LightMaximum, LightMinimum, LightStep to settings panel (#3536)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-20 23:08:23 -04:00
Chenlei Hu
ebf3c0c049 [API Nodes] Add credit management panel UI (#3535)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-20 22:11:43 -04:00
Comfy Org PR Bot
e77d5c1f57 1.17.4 (#3533)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-20 21:04:14 -04:00
Chenlei Hu
b5c1da22db [API Nodes] Remove cost from signin required dialog (#3532) 2025-04-20 17:02:42 -04:00
Chenlei Hu
0006dd3855 [Refactor] Split custom vite plugins to files under build/plugins (#3531) 2025-04-20 14:07:45 -04:00
Terry Jia
7355209c12 build vue and primevue separately and generate importmap (#3473)
Co-authored-by: Chenlei Hu <hcl@comfy.org>
2025-04-20 13:43:22 -04:00
Chenlei Hu
2aef0a9af8 [Bug] Fix model name (#3530) 2025-04-20 13:16:13 -04:00
Chenlei Hu
b74887d543 [i18n] Use gpt-o4-mini for translation (#3529) 2025-04-20 13:13:00 -04:00
Benjamin Lu
bf4ae227b3 Add manual update check (#3504)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Chenlei Hu <huchenlei@proton.me>
2025-04-20 12:28:59 -04:00
Christian Byrne
184bb582da [Manager] Check if node is core node when inferring node pack (#3521) 2025-04-20 12:13:50 -04:00
Christian Byrne
3bc3179763 Apply filename text replacements (e.g., %date:hh:mm:ss%) in all save nodes (#3523) 2025-04-20 12:12:28 -04:00
Comfy Org PR Bot
eb100894ce [chore] Update Comfy Registry API types from comfy-api@7bc8051 (#3524)
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-04-20 12:12:14 -04:00
Comfy Org PR Bot
9a992cb14d [chore] Update litegraph to 0.13.6 (#3528)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-04-21 01:18:27 +10:00
Comfy Org PR Bot
133aa9bc87 [chore] Update litegraph to 0.13.5 (#3527)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-04-20 21:31:02 +10:00
Dr.Lt.Data
3204637e5a refine locales/ko (#3526) 2025-04-20 21:14:33 +10:00
Comfy Org PR Bot
b2cb719026 1.17.3 (#3520)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-19 22:21:18 -04:00
Christian Byrne
add805460c Fix drag and drop image with embedded workflow on Firefox (#3519)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-19 22:19:48 -04:00
Benjamin Lu
9621b8f339 [Desktop] Support Nvidia Blackwell (#3480)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-04-19 21:42:30 -04:00
Christian Byrne
6be381b15d Allow git describe formatted versions of node packs in workflows (#3518) 2025-04-19 21:42:01 -04:00
Christian Byrne
8afe99f48c Fix node.widgets undefined on refresh (#3515) 2025-04-20 10:21:31 +10:00
Christian Byrne
9cd11261f9 [API Node] Set auth persistence in local stoarge (#3514) 2025-04-18 23:15:07 -04:00
Comfy Org PR Bot
fbc6665ff4 1.17.2 (#3513)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-18 22:26:07 -04:00
Christian Byrne
2daa51421c Remove fetch-templates script (#3500) 2025-04-18 20:34:03 -04:00
Christian Byrne
0f175c3dc1 [Api Node] Add ToS and privacy policy links (#3511) 2025-04-18 20:21:20 -04:00
Christian Byrne
8d4263c94e Use dev firebase and switch to prod in release workflow (#3499) 2025-04-18 17:23:10 -04:00
Chenlei Hu
04580ac031 [SettingUI] Group setting menu items (#3510) 2025-04-18 16:47:32 -04:00
Chenlei Hu
cd35f1d86d [Refactor] Generate DOM widget id in constructor (#3508) 2025-04-18 13:47:16 -04:00
Chenlei Hu
5d584577fe [Bug] Fix uuid generation in insecure context (#3505) 2025-04-18 11:43:25 -04:00
filtered
10a96d1af6 [TMP] Temporarily disable hidream template test (#3502) 2025-04-18 21:34:02 +10:00
Comfy Org PR Bot
03392a3cc7 1.17.1 (#3486)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-17 22:57:57 -04:00
Christian Byrne
12576243ad Remove unused tailwind classes (#3495) 2025-04-17 22:57:47 -04:00
Christian Byrne
e2a6dc2ec8 [Templates] Use fallbacks when translating template titles and category names (#3494) 2025-04-17 22:57:03 -04:00
Chenlei Hu
2f77d74891 [Refactor] Move tree logic to setting composable (#3491) 2025-04-17 16:34:42 -04:00
Chenlei Hu
dacb59f5d3 [Refactor] Extract setting dialog logic into composables (#3490) 2025-04-17 15:55:16 -04:00
Christian Byrne
74f991ec1b Translate Wan2.1-Fun template titles (#3489)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-17 15:55:08 -04:00
Christian Byrne
6bc03a624e Add HiDream templates translations (#3485)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-17 13:46:11 -04:00
Christian Byrne
1fb015e046 Remove templates build from release process (#3481) 2025-04-17 10:41:36 -04:00
Christian Byrne
87bf2310b6 Support previewing animated image uploads (#3479)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-17 10:20:09 -04:00
filtered
f1a25989d7 Fix corrupt workflow permanently corrupts session (#3484) 2025-04-17 10:18:14 -04:00
filtered
236e3fb3e9 Log errors in generic error handler (#3482) 2025-04-17 15:16:05 +10:00
Chenlei Hu
50382827bc [API Nodes] Add auth_token_comfy_org to queue prompt request body (#3477) 2025-04-16 15:26:25 -04:00
Christian Byrne
41675805b6 [Test] Add LGraphBadge light theme browser test (#3475)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-16 15:10:14 -04:00
Makki Shizu
6321fae6f3 fix 2_pass_pose_worship zh translation (#3474) 2025-04-16 11:04:28 -04:00
Chenlei Hu
06caa21a4d [API Nodes] Setup Google/Github login (#3471) 2025-04-15 20:56:18 -04:00
121 changed files with 7873 additions and 1060 deletions

View File

@@ -29,9 +29,9 @@ jobs:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
USE_PROD_FIREBASE_CONFIG: 'true'
run: |
npm ci
npm run fetch-templates
npm run build
npm run zipdist
- name: Upload dist artifact

View File

@@ -30,7 +30,7 @@ jobs:
with:
repository: 'Comfy-Org/ComfyUI_devtools'
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
ref: '49c8220be49120dbaff85f32813d854d6dff2d05'
ref: '9d2421fd3208a310e4d0f71fca2ea0c985759c33'
- uses: actions/setup-node@v4
with:
@@ -39,7 +39,6 @@ jobs:
- name: Build ComfyUI_frontend
run: |
npm ci
npm run fetch-templates
npm run build
working-directory: ComfyUI_frontend

View File

@@ -4,13 +4,13 @@
const { defineConfig } = require('@lobehub/i18n-cli');
module.exports = defineConfig({
modelName: 'gpt-4',
modelName: 'gpt-4.1',
splitToken: 1024,
entry: 'src/locales/en',
entryLocale: 'en',
output: 'src/locales',
outputLocales: ['zh', 'ru', 'ja', 'ko', 'fr', 'es'],
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora.
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
'latent' is the short form of 'latent space'.
'mask' is in the context of image processing.
`

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,11 @@
{
"8": {
"inputs": {
"image": "animated_web.webp"
},
"class_type": "DevToolsLoadAnimatedImageTest",
"_meta": {
"title": "Load Animated Image"
}
}
}

View File

@@ -0,0 +1,60 @@
{
"id": "3f1fcbf9-f9de-4935-8fad-401813f61b13",
"revision": 0,
"last_node_id": 10,
"last_link_id": 4,
"nodes": [
{
"id": 9,
"type": "SaveAnimatedWEBP",
"pos": [336, 104],
"size": [210, 368],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 4
}
],
"outputs": [],
"properties": {},
"widgets_values": ["ComfyUI", 6, true, 80, "default"]
},
{
"id": 10,
"type": "DevToolsLoadAnimatedImageTest",
"pos": [64, 104],
"size": [210, 316],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [4]
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "DevToolsLoadAnimatedImageTest"
},
"widgets_values": ["animated_web.webp", "image"]
}
],
"links": [[4, 10, 0, 9, 0, "IMAGE"]],
"groups": [],
"config": {},
"extra": {
"frontendVersion": "1.17.0"
},
"version": 0.4
}

View File

@@ -463,87 +463,128 @@ export class ComfyPage {
await this.nextFrame()
}
async dragAndDropFile(
fileName: string,
async dragAndDropExternalResource(
options: {
fileName?: string
url?: string
dropPosition?: Position
} = {}
) {
const { dropPosition = { x: 100, y: 100 } } = options
const { dropPosition = { x: 100, y: 100 }, fileName, url } = options
const filePath = this.assetPath(fileName)
if (!fileName && !url)
throw new Error('Must provide either fileName or url')
// Read the file content
const buffer = fs.readFileSync(filePath)
const evaluateParams: {
dropPosition: Position
fileName?: string
fileType?: string
buffer?: Uint8Array | number[]
url?: string
} = { dropPosition }
// Get file type
const getFileType = (fileName: string) => {
if (fileName.endsWith('.png')) return 'image/png'
if (fileName.endsWith('.webp')) return 'image/webp'
if (fileName.endsWith('.webm')) return 'video/webm'
if (fileName.endsWith('.json')) return 'application/json'
if (fileName.endsWith('.glb')) return 'model/gltf-binary'
return 'application/octet-stream'
// Dropping a file from the filesystem
if (fileName) {
const filePath = this.assetPath(fileName)
const buffer = fs.readFileSync(filePath)
const getFileType = (fileName: string) => {
if (fileName.endsWith('.png')) return 'image/png'
if (fileName.endsWith('.webp')) return 'image/webp'
if (fileName.endsWith('.webm')) return 'video/webm'
if (fileName.endsWith('.json')) return 'application/json'
if (fileName.endsWith('.glb')) return 'model/gltf-binary'
return 'application/octet-stream'
}
evaluateParams.fileName = fileName
evaluateParams.fileType = getFileType(fileName)
evaluateParams.buffer = [...new Uint8Array(buffer)]
}
const fileType = getFileType(fileName)
// Dropping a URL (e.g., dropping image across browser tabs in Firefox)
if (url) evaluateParams.url = url
await this.page.evaluate(
async ({ buffer, fileName, fileType, dropPosition }) => {
const file = new File([new Uint8Array(buffer)], fileName, {
type: fileType
})
const dataTransfer = new DataTransfer()
dataTransfer.items.add(file)
// Execute the drag and drop in the browser
await this.page.evaluate(async (params) => {
const dataTransfer = new DataTransfer()
const targetElement = document.elementFromPoint(
dropPosition.x,
dropPosition.y
)
if (!targetElement) {
console.error('No element found at drop position:', dropPosition)
return { success: false, error: 'No element at position' }
}
const eventOptions = {
bubbles: true,
cancelable: true,
dataTransfer,
clientX: dropPosition.x,
clientY: dropPosition.y
}
const dragOverEvent = new DragEvent('dragover', eventOptions)
const dropEvent = new DragEvent('drop', eventOptions)
Object.defineProperty(dropEvent, 'preventDefault', {
value: () => {},
writable: false
})
Object.defineProperty(dropEvent, 'stopPropagation', {
value: () => {},
writable: false
})
targetElement.dispatchEvent(dragOverEvent)
targetElement.dispatchEvent(dropEvent)
return {
success: true,
targetInfo: {
tagName: targetElement.tagName,
id: targetElement.id,
classList: Array.from(targetElement.classList)
// Add file if provided
if (params.buffer && params.fileName && params.fileType) {
const file = new File(
[new Uint8Array(params.buffer)],
params.fileName,
{
type: params.fileType
}
)
dataTransfer.items.add(file)
}
// Add URL data if provided
if (params.url) {
dataTransfer.setData('text/uri-list', params.url)
dataTransfer.setData('text/x-moz-url', params.url)
}
const targetElement = document.elementFromPoint(
params.dropPosition.x,
params.dropPosition.y
)
if (!targetElement) {
console.error('No element found at drop position:', params.dropPosition)
return { success: false, error: 'No element at position' }
}
const eventOptions = {
bubbles: true,
cancelable: true,
dataTransfer,
clientX: params.dropPosition.x,
clientY: params.dropPosition.y
}
const dragOverEvent = new DragEvent('dragover', eventOptions)
const dropEvent = new DragEvent('drop', eventOptions)
Object.defineProperty(dropEvent, 'preventDefault', {
value: () => {},
writable: false
})
Object.defineProperty(dropEvent, 'stopPropagation', {
value: () => {},
writable: false
})
targetElement.dispatchEvent(dragOverEvent)
targetElement.dispatchEvent(dropEvent)
return {
success: true,
targetInfo: {
tagName: targetElement.tagName,
id: targetElement.id,
classList: Array.from(targetElement.classList)
}
},
{ buffer: [...new Uint8Array(buffer)], fileName, fileType, dropPosition }
)
}
}, evaluateParams)
await this.nextFrame()
}
async dragAndDropFile(
fileName: string,
options: { dropPosition?: Position } = {}
) {
return this.dragAndDropExternalResource({ fileName, ...options })
}
async dragAndDropURL(url: string, options: { dropPosition?: Position } = {}) {
return this.dragAndDropExternalResource({ url, ...options })
}
async dragNode2() {
await this.dragAndDrop({ x: 622, y: 400 }, { x: 622, y: 300 })
await this.nextFrame()
@@ -885,7 +926,7 @@ export class ComfyPage {
async getNodeRefById(id: NodeId) {
return new NodeReference(id, this)
}
async getNodes() {
async getNodes(): Promise<LGraphNode[]> {
return await this.page.evaluate(() => {
return window['app'].graph.nodes
})

View File

@@ -341,3 +341,30 @@ test.describe('Error dialog', () => {
await expect(errorDialog).toBeVisible()
})
})
test.describe('Signin dialog', () => {
test('Paste content to signin dialog should not paste node on canvas', async ({
comfyPage
}) => {
const nodeNum = (await comfyPage.getNodes()).length
await comfyPage.clickEmptyLatentNode()
await comfyPage.ctrlC()
const textBox = comfyPage.widgetTextBox
await textBox.click()
await textBox.fill('test_password')
await textBox.press('Control+a')
await textBox.press('Control+c')
await comfyPage.page.evaluate(() => {
window['app'].extensionManager.dialog.showSignInDialog()
})
const emailInput = comfyPage.page.locator('#comfy-org-sign-in-password')
await emailInput.waitFor({ state: 'visible' })
await emailInput.press('Control+v')
await expect(emailInput).toHaveValue('test_password')
expect(await comfyPage.getNodes()).toHaveLength(nodeNum)
})
})

View File

@@ -24,4 +24,11 @@ test.describe('DOM Widget', () => {
await expect(firstMultiline).not.toBeVisible()
await expect(lastMultiline).not.toBeVisible()
})
test('Position update when entering focus mode', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.executeCommand('Workspace.ToggleFocusMode')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('focus-mode-on.png')
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

@@ -3,17 +3,35 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Load Workflow in Media', () => {
;[
const fileNames = [
'workflow.webp',
'edited_workflow.webp',
'no_workflow.webp',
'large_workflow.webp',
'workflow.webm',
'workflow.glb'
].forEach(async (fileName) => {
test(`Load workflow in ${fileName}`, async ({ comfyPage }) => {
]
fileNames.forEach(async (fileName) => {
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({
comfyPage
}) => {
await comfyPage.dragAndDropFile(fileName)
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
})
})
const urls = [
'https://comfyanonymous.github.io/ComfyUI_examples/hidream/hidream_dev_example.png'
]
urls.forEach(async (url) => {
test(`Load workflow from URL${url} (drop from different browser tabs)`, async ({
comfyPage
}) => {
await comfyPage.dragAndDropURL(url)
const readableName = url.split('/').pop()
await expect(comfyPage.canvas).toHaveScreenshot(
`dropped_workflow_url_${readableName}.png`
)
})
})
})

View File

@@ -92,4 +92,20 @@ test.describe('Node badge color', () => {
'node-badge-unknown-color-palette.png'
)
})
test('Can show node badge with light color palette', async ({
comfyPage
}) => {
await comfyPage.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll
)
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame()
// Click empty space to trigger canvas re-render.
await comfyPage.clickEmptySpace()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-badge-light-color-palette.png'
)
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 117 KiB

View File

@@ -1,8 +1,17 @@
import { expect } from '@playwright/test'
import fs from 'fs'
import { Page, expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
async function checkTemplateFileExists(
page: Page,
filename: string
): Promise<boolean> {
const response = await page.request.head(
new URL(`/templates/${filename}`, page.url()).toString()
)
return response.ok()
}
test.describe('Templates', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
@@ -12,32 +21,32 @@ test.describe('Templates', () => {
test('should have a JSON workflow file for each template', async ({
comfyPage
}) => {
test.slow()
const templates = await comfyPage.templates.getAllTemplates()
for (const template of templates) {
const workflowPath = comfyPage.templates.getTemplatePath(
const exists = await checkTemplateFileExists(
comfyPage.page,
`${template.name}.json`
)
expect(
fs.existsSync(workflowPath),
`Missing workflow: ${template.name}`
).toBe(true)
expect(exists, `Missing workflow: ${template.name}`).toBe(true)
}
})
test('should have all required thumbnail media for each template', async ({
comfyPage
}) => {
test.slow()
const templates = await comfyPage.templates.getAllTemplates()
for (const template of templates) {
const { name, mediaSubtype, thumbnailVariant } = template
const baseMedia = `${name}-1.${mediaSubtype}`
const basePath = comfyPage.templates.getTemplatePath(baseMedia)
// Check base thumbnail
expect(
fs.existsSync(basePath),
`Missing base thumbnail: ${baseMedia}`
).toBe(true)
const baseExists = await checkTemplateFileExists(
comfyPage.page,
baseMedia
)
expect(baseExists, `Missing base thumbnail: ${baseMedia}`).toBe(true)
// Check second thumbnail for variants that need it
if (
@@ -45,9 +54,12 @@ test.describe('Templates', () => {
thumbnailVariant === 'hoverDissolve'
) {
const secondMedia = `${name}-2.${mediaSubtype}`
const secondPath = comfyPage.templates.getTemplatePath(secondMedia)
const secondExists = await checkTemplateFileExists(
comfyPage.page,
secondMedia
)
expect(
fs.existsSync(secondPath),
secondExists,
`Missing second thumbnail: ${secondMedia} required for ${thumbnailVariant}`
).toBe(true)
}
@@ -86,4 +98,48 @@ test.describe('Templates', () => {
// Expect the templates dialog to be shown
expect(await comfyPage.templates.content.isVisible()).toBe(true)
})
test('Uses title field as fallback when the key is not found in locales', async ({
comfyPage
}) => {
// Capture request for the index.json
await comfyPage.page.route('**/templates/index.json', async (route, _) => {
// Add a new template that won't have a translation pre-generated
const response = [
{
moduleName: 'default',
title: 'FALLBACK CATEGORY',
type: 'image',
templates: [
{
name: 'unknown_key_has_no_translation_available',
title: 'FALLBACK TEMPLATE NAME',
mediaType: 'image',
mediaSubtype: 'webp',
description: 'No translations found'
}
]
}
]
await route.fulfill({
status: 200,
body: JSON.stringify(response),
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store'
}
})
})
// Load the templates dialog
await comfyPage.executeCommand('Comfy.BrowseTemplates')
// Expect the title to be used as fallback for template cards
await expect(
comfyPage.templates.content.getByText('FALLBACK TEMPLATE NAME')
).toBeVisible()
// Expect the title to be used as fallback for the template categories
await expect(comfyPage.page.getByLabel('FALLBACK CATEGORY')).toBeVisible()
})
})

View File

@@ -186,6 +186,105 @@ test.describe('Image widget', () => {
})
})
test.describe('Animated image widget', () => {
test('Shows preview of uploaded animated image', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/load_animated_webp')
// Get position of the load animated webp node
const nodes = await comfyPage.getNodeRefsByType(
'DevToolsLoadAnimatedImageTest'
)
const loadAnimatedWebpNode = nodes[0]
const { x, y } = await loadAnimatedWebpNode.getPosition()
// Drag and drop image file onto the load animated webp node
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y }
})
// Expect the image preview to change automatically
await expect(comfyPage.canvas).toHaveScreenshot(
'animated_image_preview_drag_and_dropped.png'
)
// Wait for animation to go to next frame
await comfyPage.page.waitForTimeout(512)
// Move mouse and click on canvas to trigger render
await comfyPage.page.mouse.click(64, 64)
// Expect the image preview to change to the next frame of the animation
await expect(comfyPage.canvas).toHaveScreenshot(
'animated_image_preview_drag_and_dropped_next_frame.png'
)
})
test('Can drag-and-drop animated webp image', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/load_animated_webp')
// Get position of the load animated webp node
const nodes = await comfyPage.getNodeRefsByType(
'DevToolsLoadAnimatedImageTest'
)
const loadAnimatedWebpNode = nodes[0]
const { x, y } = await loadAnimatedWebpNode.getPosition()
// Drag and drop image file onto the load animated webp node
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y }
})
// Expect the filename combo value to be updated
const fileComboWidget = await loadAnimatedWebpNode.getWidget(0)
const filename = await fileComboWidget.getValue()
expect(filename).toContain('animated_webp.webp')
})
test('Can preview saved animated webp image', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/save_animated_webp')
// Get position of the load animated webp node
const loadNodes = await comfyPage.getNodeRefsByType(
'DevToolsLoadAnimatedImageTest'
)
const loadAnimatedWebpNode = loadNodes[0]
const { x, y } = await loadAnimatedWebpNode.getPosition()
// Drag and drop image file onto the load animated webp node
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y }
})
await comfyPage.nextFrame()
// Get the SaveAnimatedWEBP node
const saveNodes = await comfyPage.getNodeRefsByType('SaveAnimatedWEBP')
const saveAnimatedWebpNode = saveNodes[0]
if (!saveAnimatedWebpNode)
throw new Error('SaveAnimatedWEBP node not found')
// Simulate the graph executing
await comfyPage.page.evaluate(
([loadId, saveId]) => {
// Set the output of the SaveAnimatedWEBP node to equal the loader node's image
window['app'].nodeOutputs[saveId] = window['app'].nodeOutputs[loadId]
},
[loadAnimatedWebpNode.id, saveAnimatedWebpNode.id]
)
await comfyPage.nextFrame()
// Wait for animation to go to next frame
await comfyPage.page.waitForTimeout(512)
// Move mouse and click on canvas to trigger render
await comfyPage.page.mouse.click(64, 64)
// Expect the SaveAnimatedWEBP node to have an output preview
await expect(comfyPage.canvas).toHaveScreenshot(
'animated_image_preview_saved_webp.png'
)
})
})
test.describe('Load audio widget', () => {
test('Can load audio', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/load_audio_widget')

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View File

@@ -0,0 +1,59 @@
import { Plugin } from 'vite'
/**
* Vite plugin that adds an alias export for Vue's createBaseVNode as createElementVNode.
*
* This plugin addresses compatibility issues where some components or libraries
* might be using the older createElementVNode function name instead of createBaseVNode.
* It modifies the Vue vendor chunk during build to add the alias export.
*
* @returns {Plugin} A Vite plugin that modifies the Vue vendor chunk exports
*/
export function addElementVnodeExportPlugin(): Plugin {
return {
name: 'add-element-vnode-export-plugin',
renderChunk(code, chunk, _options) {
if (chunk.name.startsWith('vendor-vue')) {
const exportRegex = /(export\s*\{)([^}]*)(\}\s*;?\s*)$/
const match = code.match(exportRegex)
if (match) {
const existingExports = match[2].trim()
const exportsArray = existingExports
.split(',')
.map((e) => e.trim())
.filter(Boolean)
const hasCreateBaseVNode = exportsArray.some((e) =>
e.startsWith('createBaseVNode')
)
const hasCreateElementVNode = exportsArray.some((e) =>
e.includes('createElementVNode')
)
if (hasCreateBaseVNode && !hasCreateElementVNode) {
const newExportStatement = `${match[1]} ${existingExports ? existingExports + ',' : ''} createBaseVNode as createElementVNode ${match[3]}`
const newCode = code.replace(exportRegex, newExportStatement)
console.log(
`[add-element-vnode-export-plugin] Added 'createBaseVNode as createElementVNode' export to vendor-vue chunk.`
)
return { code: newCode, map: null }
} else if (!hasCreateBaseVNode) {
console.warn(
`[add-element-vnode-export-plugin] Warning: 'createBaseVNode' not found in exports of vendor-vue chunk. Cannot add alias.`
)
}
} else {
console.warn(
`[add-element-vnode-export-plugin] Warning: Could not find expected export block format in vendor-vue chunk.`
)
}
}
return null
}
}
}

View File

@@ -0,0 +1,82 @@
import path from 'path'
import { Plugin } from 'vite'
interface ShimResult {
code: string
exports: string[]
}
function isLegacyFile(id: string): boolean {
return (
id.endsWith('.ts') &&
(id.includes('src/extensions/core') || id.includes('src/scripts'))
)
}
function transformExports(code: string, id: string): ShimResult {
const moduleName = getModuleName(id)
const exports: string[] = []
let newCode = code
// Regex to match different types of exports
const regex =
/export\s+(const|let|var|function|class|async function)\s+([a-zA-Z$_][a-zA-Z\d$_]*)(\s|\()/g
let match
while ((match = regex.exec(code)) !== null) {
const name = match[2]
// All exports should be bind to the window object as new API endpoint.
if (exports.length == 0) {
newCode += `\nwindow.comfyAPI = window.comfyAPI || {};`
newCode += `\nwindow.comfyAPI.${moduleName} = window.comfyAPI.${moduleName} || {};`
}
newCode += `\nwindow.comfyAPI.${moduleName}.${name} = ${name};`
exports.push(
`export const ${name} = window.comfyAPI.${moduleName}.${name};\n`
)
}
return {
code: newCode,
exports
}
}
function getModuleName(id: string): string {
// Simple example to derive a module name from the file path
const parts = id.split('/')
const fileName = parts[parts.length - 1]
return fileName.replace(/\.\w+$/, '') // Remove file extension
}
export function comfyAPIPlugin(isDev: boolean): Plugin {
return {
name: 'comfy-api-plugin',
transform(code: string, id: string) {
if (isDev) return null
if (isLegacyFile(id)) {
const result = transformExports(code, id)
if (result.exports.length > 0) {
const projectRoot = process.cwd()
const relativePath = path.relative(path.join(projectRoot, 'src'), id)
const shimFileName = relativePath.replace(/\.ts$/, '.js')
const shimComment = `// Shim for ${relativePath}\n`
this.emitFile({
type: 'asset',
fileName: shimFileName,
source: shimComment + result.exports.join('')
})
}
return {
code: result.code,
map: null // If you're not modifying the source map, return null
}
}
}
}
}

View File

@@ -0,0 +1,103 @@
import type { OutputOptions } from 'rollup'
import { HtmlTagDescriptor, Plugin } from 'vite'
interface VendorLibrary {
name: string
pattern: RegExp
}
/**
* Vite plugin that generates an import map for vendor chunks.
*
* This plugin creates a browser-compatible import map that maps module specifiers
* (like 'vue' or 'primevue') to their actual file locations in the build output.
* This improves module loading in modern browsers and enables better caching.
*
* The plugin:
* 1. Tracks vendor chunks during bundle generation
* 2. Creates mappings between module names and their file paths
* 3. Injects an import map script tag into the HTML head
* 4. Configures manual chunk splitting for vendor libraries
*
* @param vendorLibraries - An array of vendor libraries to split into separate chunks
* @returns {Plugin} A Vite plugin that generates and injects an import map
*/
export function generateImportMapPlugin(
vendorLibraries: VendorLibrary[]
): Plugin {
const importMapEntries: Record<string, string> = {}
return {
name: 'generate-import-map-plugin',
// Configure manual chunks during the build process
configResolved(config) {
if (config.build) {
// Ensure rollupOptions exists
if (!config.build.rollupOptions) {
config.build.rollupOptions = {}
}
const outputOptions: OutputOptions = {
manualChunks: (id: string) => {
for (const lib of vendorLibraries) {
if (lib.pattern.test(id)) {
return `vendor-${lib.name}`
}
}
return null
},
// Disable minification of internal exports to preserve function names
minifyInternalExports: false
}
config.build.rollupOptions.output = outputOptions
}
},
generateBundle(_options, bundle) {
for (const fileName in bundle) {
const chunk = bundle[fileName]
if (chunk.type === 'chunk' && !chunk.isEntry) {
// Find matching vendor library by chunk name
const vendorLib = vendorLibraries.find(
(lib) => chunk.name === `vendor-${lib.name}`
)
if (vendorLib) {
const relativePath = `./${chunk.fileName.replace(/\\/g, '/')}`
importMapEntries[vendorLib.name] = relativePath
console.log(
`[ImportMap Plugin] Found chunk: ${chunk.name} -> Mapped '${vendorLib.name}' to '${relativePath}'`
)
}
}
}
},
transformIndexHtml(html) {
if (Object.keys(importMapEntries).length === 0) {
console.warn(
'[ImportMap Plugin] No vendor chunks found to create import map.'
)
return html
}
const importMap = {
imports: importMapEntries
}
const importMapTag: HtmlTagDescriptor = {
tag: 'script',
attrs: { type: 'importmap' },
children: JSON.stringify(importMap, null, 2),
injectTo: 'head'
}
return {
html,
tags: [importMapTag]
}
}
}
}

3
build/plugins/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export { addElementVnodeExportPlugin } from './addElementVnodeExportPlugin'
export { comfyAPIPlugin } from './comfyAPIPlugin'
export { generateImportMapPlugin } from './generateImportMapPlugin'

1
global.d.ts vendored
View File

@@ -3,6 +3,7 @@ declare const __SENTRY_ENABLED__: boolean
declare const __SENTRY_DSN__: string
declare const __ALGOLIA_APP_ID__: string
declare const __ALGOLIA_API_KEY__: string
declare const __USE_PROD_FIREBASE_CONFIG__: boolean
interface Navigator {
/**

20
package-lock.json generated
View File

@@ -1,18 +1,18 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.17.0",
"version": "1.17.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@comfyorg/comfyui-frontend",
"version": "1.17.0",
"version": "1.17.7",
"license": "GPL-3.0-only",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.31",
"@comfyorg/litegraph": "^0.13.3",
"@comfyorg/comfyui-electron-types": "^0.4.39",
"@comfyorg/litegraph": "^0.13.8",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",
@@ -476,15 +476,15 @@
}
},
"node_modules/@comfyorg/comfyui-electron-types": {
"version": "0.4.31",
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.31.tgz",
"integrity": "sha512-6tdUfrRyJ9mLlGhNxKqao0kdO+nKRLzQIbENmTK1EtJ1zhMmCp43a+pG7+kecjgp0pbfzxWKhTdCarS9A9fkqw==",
"version": "0.4.39",
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.39.tgz",
"integrity": "sha512-5ZPaXy3SMMi5YO2gjgUWrRhmh/Ble1Qh//s+QKMyDP+FbMvVJu5eq6kc/5NiYngzaWziCcthgAlDBDz5oV5j4A==",
"license": "GPL-3.0-only"
},
"node_modules/@comfyorg/litegraph": {
"version": "0.13.3",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.13.3.tgz",
"integrity": "sha512-vkuVyA4RFDEXNHILGN7JlldZqtCqxfTi/T6O+Jv3KVSMSDbwkR8i7/BqAb2y6yaaxK2XktzwX0T7Q0ToNJ8G1A==",
"version": "0.13.8",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.13.8.tgz",
"integrity": "sha512-NpnQpCM0rkAuiWSt7Yp2U9RmNqnK7L9xOye0xHu/avzdOMQTY6vy0g7VKopUrTSGMpeFbgYcnUh/W/XZcCx+sg==",
"license": "MIT"
},
"node_modules/@cspotcode/source-map-support": {

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.17.0",
"version": "1.17.7",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -25,8 +25,7 @@
"lint:fix": "eslint src --fix",
"locale": "lobe-i18n locale",
"collect-i18n": "playwright test --config=playwright.i18n.config.ts",
"json-schema": "tsx scripts/generate-json-schema.ts",
"fetch-templates": "tsx scripts/fetch-templates.ts"
"json-schema": "tsx scripts/generate-json-schema.ts"
},
"devDependencies": {
"@eslint/js": "^9.8.0",
@@ -72,8 +71,8 @@
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.31",
"@comfyorg/litegraph": "^0.13.3",
"@comfyorg/comfyui-electron-types": "^0.4.39",
"@comfyorg/litegraph": "^0.13.8",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",

View File

@@ -1,24 +0,0 @@
import fs from 'fs-extra'
import { execSync } from 'node:child_process'
import path from 'node:path'
const workflowTemplatesRepo = 'https://github.com/Comfy-Org/workflow_templates'
const tempRepoDir = './templates_repo'
// Clone the repository
execSync(`git clone ${workflowTemplatesRepo} --depth 1 ${tempRepoDir}`)
// Create public/templates directory if it doesn't exist
fs.ensureDirSync('public/templates')
// Copy templates from repo to public/templates
const sourceDir = path.join(tempRepoDir, 'templates')
const targetDir = 'public/templates'
// Copy entire directory at once
fs.copySync(sourceDir, targetDir)
// Remove the temporary repository directory
fs.removeSync(tempRepoDir)
console.log('Templates fetched successfully')

View File

@@ -0,0 +1,31 @@
<template>
<div class="flex flex-col gap-3 h-full">
<div class="flex text-xs">
<div>{{ t('apiNodesCostBreakdown.title') }}</div>
</div>
<ScrollPanel class="flex-grow h-0">
<div class="flex flex-col gap-2">
<div
v-for="nodeName in nodeNames"
:key="nodeName"
class="flex items-center justify-between px-3 py-2 rounded-md bg-[var(--p-content-border-color)]"
>
<div class="flex items-center gap-2">
<span class="text-base font-medium leading-tight">{{
nodeName
}}</span>
</div>
</div>
</div>
</ScrollPanel>
</div>
</template>
<script setup lang="ts">
import ScrollPanel from 'primevue/scrollpanel'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const { nodeNames } = defineProps<{ nodeNames: string[] }>()
</script>

View File

@@ -9,7 +9,7 @@
{{ t('apiNodesSignInDialog.message') }}
</div>
<ApiNodesCostBreakdown :nodes="apiNodes" :show-total="true" />
<ApiNodesList :node-names="apiNodeNames" />
<div class="flex justify-between items-center">
<Button :label="t('g.learnMore')" link />
@@ -30,13 +30,10 @@
import Button from 'primevue/button'
import { useI18n } from 'vue-i18n'
import ApiNodesCostBreakdown from '@/components/common/ApiNodesCostBreakdown.vue'
import type { ApiNodeCost } from '@/types/apiNodeTypes'
const { t } = useI18n()
const { apiNodes, onLogin, onCancel } = defineProps<{
apiNodes: ApiNodeCost[]
const { apiNodeNames, onLogin, onCancel } = defineProps<{
apiNodeNames: string[]
onLogin?: () => void
onCancel?: () => void
}>()

View File

@@ -10,15 +10,21 @@
/>
<Listbox
v-model="activeCategory"
:options="categories"
:options="groupedMenuTreeNodes"
option-label="translatedLabel"
option-group-label="label"
option-group-children="children"
scroll-height="100%"
:option-disabled="
(option: SettingTreeNode) =>
!queryIsEmpty && !searchResultsCategories.has(option.label ?? '')
"
class="border-none w-full"
/>
>
<template #optiongroup>
<Divider class="my-0" />
</template>
</Listbox>
</ScrollPanel>
<Divider layout="vertical" class="mx-1 2xl:mx-4 hidden md:flex" />
<Divider layout="horizontal" class="flex md:hidden" />
@@ -42,6 +48,8 @@
</PanelTemplate>
<AboutPanel />
<UserPanel />
<CreditsPanel />
<Suspense>
<KeybindingPanel />
<template #fallback>
@@ -73,30 +81,32 @@ import Listbox from 'primevue/listbox'
import ScrollPanel from 'primevue/scrollpanel'
import TabPanels from 'primevue/tabpanels'
import Tabs from 'primevue/tabs'
import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { computed, defineAsyncComponent, watch } from 'vue'
import SearchBox from '@/components/common/SearchBox.vue'
import { st } from '@/i18n'
import {
SettingTreeNode,
getSettingInfo,
useSettingStore
} from '@/stores/settingStore'
import { useSettingSearch } from '@/composables/setting/useSettingSearch'
import { useSettingUI } from '@/composables/setting/useSettingUI'
import { SettingTreeNode } from '@/stores/settingStore'
import { ISettingGroup, SettingParams } from '@/types/settingTypes'
import { isElectron } from '@/utils/envUtil'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { flattenTree } from '@/utils/treeUtil'
import AboutPanel from './setting/AboutPanel.vue'
import ColorPaletteMessage from './setting/ColorPaletteMessage.vue'
import CreditsPanel from './setting/CreditsPanel.vue'
import CurrentUserMessage from './setting/CurrentUserMessage.vue'
import FirstTimeUIMessage from './setting/FirstTimeUIMessage.vue'
import PanelTemplate from './setting/PanelTemplate.vue'
import SettingsPanel from './setting/SettingsPanel.vue'
import UserPanel from './setting/UserPanel.vue'
const props = defineProps<{
defaultPanel?: 'about' | 'keybinding' | 'extension' | 'server-config'
const { defaultPanel } = defineProps<{
defaultPanel?:
| 'about'
| 'keybinding'
| 'extension'
| 'server-config'
| 'user'
| 'credits'
}>()
const KeybindingPanel = defineAsyncComponent(
@@ -109,71 +119,23 @@ const ServerConfigPanel = defineAsyncComponent(
() => import('./setting/ServerConfigPanel.vue')
)
const aboutPanelNode: SettingTreeNode = {
key: 'about',
label: 'About',
children: []
}
const {
activeCategory,
defaultCategory,
settingCategories,
groupedMenuTreeNodes
} = useSettingUI(defaultPanel)
const keybindingPanelNode: SettingTreeNode = {
key: 'keybinding',
label: 'Keybinding',
children: []
}
const extensionPanelNode: SettingTreeNode = {
key: 'extension',
label: 'Extension',
children: []
}
const serverConfigPanelNode: SettingTreeNode = {
key: 'server-config',
label: 'Server-Config',
children: []
}
/**
* Server config panel is only available in Electron. We might want to support
* it in the web version in the future.
*/
const serverConfigPanelNodeList = computed<SettingTreeNode[]>(() => {
return isElectron() ? [serverConfigPanelNode] : []
})
const settingStore = useSettingStore()
const settingRoot = computed<SettingTreeNode>(() => settingStore.settingTree)
const settingCategories = computed<SettingTreeNode[]>(
() => settingRoot.value.children ?? []
)
const { t } = useI18n()
const categories = computed<SettingTreeNode[]>(() =>
[
...settingCategories.value,
keybindingPanelNode,
extensionPanelNode,
...serverConfigPanelNodeList.value,
aboutPanelNode
].map((node) => ({
...node,
translatedLabel: t(
`settingsCategories.${normalizeI18nKey(node.label)}`,
node.label
)
}))
)
const activeCategory = ref<SettingTreeNode | null>(null)
const getDefaultCategory = () => {
return props.defaultPanel
? categories.value.find((x) => x.key === props.defaultPanel) ??
categories.value[0]
: categories.value[0]
}
onMounted(() => {
activeCategory.value = getDefaultCategory()
})
const {
searchQuery,
searchResultsCategories,
queryIsEmpty,
inSearch,
handleSearch: handleSearchBase,
getSearchResults
} = useSettingSearch()
// Sort groups for a category
const sortedGroups = (category: SettingTreeNode): ISettingGroup[] => {
return [...(category.children ?? [])]
.sort((a, b) => a.label.localeCompare(b.label))
@@ -183,92 +145,20 @@ const sortedGroups = (category: SettingTreeNode): ISettingGroup[] => {
}))
}
const searchQuery = ref<string>('')
const filteredSettingIds = ref<string[]>([])
const searchInProgress = ref<boolean>(false)
watch(searchQuery, () => (searchInProgress.value = true))
const searchResults = computed<ISettingGroup[]>(() => {
const groupedSettings: { [key: string]: SettingParams[] } = {}
filteredSettingIds.value.forEach((id) => {
const setting = settingStore.settingsById[id]
const info = getSettingInfo(setting)
const groupLabel = info.subCategory
if (
activeCategory.value === null ||
activeCategory.value.label === info.category
) {
if (!groupedSettings[groupLabel]) {
groupedSettings[groupLabel] = []
}
groupedSettings[groupLabel].push(setting)
}
})
return Object.entries(groupedSettings).map(([label, settings]) => ({
label,
settings
}))
})
/**
* Settings categories that contains at least one setting in search results.
*/
const searchResultsCategories = computed<Set<string>>(() => {
return new Set(
filteredSettingIds.value.map(
(id) => getSettingInfo(settingStore.settingsById[id]).category
)
)
})
const handleSearch = (query: string) => {
if (!query) {
filteredSettingIds.value = []
activeCategory.value ??= getDefaultCategory()
return
}
const queryLower = query.toLocaleLowerCase()
const allSettings = flattenTree<SettingParams>(settingRoot.value)
const filteredSettings = allSettings.filter((setting) => {
const idLower = setting.id.toLowerCase()
const nameLower = setting.name.toLowerCase()
const translatedName = st(
`settings.${normalizeI18nKey(setting.id)}.name`,
setting.name
).toLocaleLowerCase()
const info = getSettingInfo(setting)
const translatedCategory = st(
`settingsCategories.${normalizeI18nKey(info.category)}`,
info.category
).toLocaleLowerCase()
const translatedSubCategory = st(
`settingsCategories.${normalizeI18nKey(info.subCategory)}`,
info.subCategory
).toLocaleLowerCase()
return (
idLower.includes(queryLower) ||
nameLower.includes(queryLower) ||
translatedName.includes(queryLower) ||
translatedCategory.includes(queryLower) ||
translatedSubCategory.includes(queryLower)
)
})
filteredSettingIds.value = filteredSettings.map((x) => x.id)
searchInProgress.value = false
activeCategory.value = null
handleSearchBase(query)
activeCategory.value = query ? null : defaultCategory.value
}
const queryIsEmpty = computed(() => searchQuery.value.length === 0)
const inSearch = computed(() => !queryIsEmpty.value && !searchInProgress.value)
// Get search results
const searchResults = computed<ISettingGroup[]>(() =>
getSearchResults(activeCategory.value)
)
const tabValue = computed<string>(() =>
inSearch.value ? 'Search Results' : activeCategory.value?.label ?? ''
)
// Don't allow null category to be set outside of search.
// In search mode, the active category can be null to show all search results.
watch(activeCategory, (_, oldValue) => {
@@ -313,14 +203,8 @@ watch(activeCategory, (_, oldValue) => {
}
}
/* Show a separator line above the Keybinding tab */
/* This indicates the start of custom setting panels */
.settings-sidebar :deep(.p-listbox-option[aria-label='Keybinding']) {
position: relative;
}
.settings-sidebar :deep(.p-listbox-option[aria-label='Keybinding'])::before {
@apply content-[''] top-0 left-0 absolute w-full;
border-top: 1px solid var(--p-divider-border-color);
/* Hide the first group separator */
.settings-sidebar :deep(.p-listbox-option-group:nth-child(1)) {
display: none;
}
</style>

View File

@@ -61,14 +61,22 @@
<!-- Terms -->
<p class="text-xs text-muted mt-8">
{{ t('auth.login.termsText') }}
<span class="text-blue-500 cursor-pointer">{{
t('auth.login.termsLink')
}}</span>
<a
href="https://www.comfy.org/terms-of-service"
target="_blank"
class="text-blue-500 cursor-pointer"
>
{{ t('auth.login.termsLink') }}
</a>
{{ t('auth.login.andText') }}
<span class="text-blue-500 cursor-pointer">{{
t('auth.login.privacyLink')
}}</span
>.
<a
href="https://www.comfy.org/privacy"
target="_blank"
class="text-blue-500 cursor-pointer"
>
{{ t('auth.login.privacyLink') }}
</a>
.
</p>
</div>
</template>
@@ -79,6 +87,7 @@ import Divider from 'primevue/divider'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { SignInData, SignUpData } from '@/schemas/signInSchema'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
@@ -92,33 +101,32 @@ const { onSuccess } = defineProps<{
}>()
const firebaseAuthStore = useFirebaseAuthStore()
const { wrapWithErrorHandlingAsync } = useErrorHandling()
const isSignIn = ref(true)
const toggleState = () => {
isSignIn.value = !isSignIn.value
}
const signInWithGoogle = () => {
// Implement Google login
console.log(isSignIn.value)
console.log('Google login clicked')
const signInWithGoogle = wrapWithErrorHandlingAsync(async () => {
await firebaseAuthStore.loginWithGoogle()
onSuccess()
}
})
const signInWithGithub = () => {
// Implement Github login
console.log(isSignIn.value)
console.log('Github login clicked')
const signInWithGithub = wrapWithErrorHandlingAsync(async () => {
await firebaseAuthStore.loginWithGithub()
onSuccess()
}
})
const signInWithEmail = async (values: SignInData | SignUpData) => {
const { email, password } = values
if (isSignIn.value) {
await firebaseAuthStore.login(email, password)
} else {
await firebaseAuthStore.register(email, password)
const signInWithEmail = wrapWithErrorHandlingAsync(
async (values: SignInData | SignUpData) => {
const { email, password } = values
if (isSignIn.value) {
await firebaseAuthStore.login(email, password)
} else {
await firebaseAuthStore.register(email, password)
}
onSuccess()
}
onSuccess()
}
)
</script>

View File

@@ -0,0 +1,47 @@
<template>
<div class="flex flex-col items-center gap-4 p-4">
<div class="flex flex-col items-center text-center">
<i class="pi pi-exclamation-circle mb-4" style="font-size: 2rem" />
<h2 class="text-2xl font-semibold mb-2">
{{ $t(`auth.required.${type}.title`) }}
</h2>
<p class="text-gray-600 mb-4 max-w-md">
{{ $t(`auth.required.${type}.message`) }}
</p>
</div>
<div class="flex gap-4">
<Button
class="w-60"
severity="primary"
:label="$t(`auth.required.${type}.action`)"
@click="openPanel"
/>
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { useDialogStore } from '@/stores/dialogStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
const props = defineProps<{
type: 'signIn' | 'credits'
}>()
const dialogStore = useDialogStore()
const authStore = useFirebaseAuthStore()
const openPanel = () => {
// Close the current dialog
dialogStore.closeDialog({ key: 'signin-required' })
// Open user settings and navigate to appropriate panel
if (props.type === 'credits') {
authStore.openCreditsPanel()
} else {
authStore.openSignInPanel()
}
}
</script>

View File

@@ -0,0 +1,158 @@
<template>
<div class="flex flex-col p-6">
<div
class="flex items-center gap-2"
:class="{ 'text-red-500': isInsufficientCredits }"
>
<i
:class="[
'text-2xl',
isInsufficientCredits ? 'pi pi-exclamation-triangle' : ''
]"
/>
<h2 class="text-2xl font-semibold">
{{
$t(
isInsufficientCredits
? 'credits.topUp.insufficientTitle'
: 'credits.topUp.title'
)
}}
</h2>
</div>
<!-- Error Message -->
<p v-if="isInsufficientCredits" class="text-lg text-muted mt-6">
{{ $t('credits.topUp.insufficientMessage') }}
</p>
<!-- Balance Section -->
<div class="flex justify-between items-center mt-8">
<div class="flex flex-col gap-2">
<div class="text-muted">{{ $t('credits.yourCreditBalance') }}</div>
<div class="flex items-center gap-2">
<Tag
severity="secondary"
icon="pi pi-dollar"
rounded
class="text-amber-400 p-1"
/>
<span class="text-2xl">{{ formattedBalance }}</span>
</div>
</div>
<Button
text
severity="secondary"
:label="$t('credits.creditsHistory')"
icon="pi pi-arrow-up-right"
@click="handleSeeDetails"
/>
</div>
<!-- Amount Input Section -->
<div class="flex flex-col gap-2 mt-8">
<div>
<span class="text-muted">{{ $t('credits.topUp.addCredits') }}</span>
<span class="text-muted text-sm ml-1">{{
$t('credits.topUp.maxAmount')
}}</span>
</div>
<div class="flex items-center gap-2">
<Tag
severity="secondary"
icon="pi pi-dollar"
rounded
class="text-amber-400 p-1"
/>
<InputNumber
v-model="amount"
:min="1"
:max="1000"
:step="1"
mode="currency"
currency="USD"
show-buttons
@blur="handleBlur"
@input="handleInput"
/>
</div>
</div>
<div class="flex justify-end mt-8">
<ProgressSpinner v-if="loading" class="w-8 h-8" />
<Button
v-else
severity="primary"
:label="$t('credits.topUp.buyNow')"
:disabled="!amount || amount > 1000"
:pt="{
root: { class: 'px-8' }
}"
@click="handleBuyNow"
/>
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import InputNumber from 'primevue/inputnumber'
import ProgressSpinner from 'primevue/progressspinner'
import Tag from 'primevue/tag'
import { computed, onBeforeUnmount, ref } from 'vue'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { formatMetronomeCurrency, usdToMicros } from '@/utils/formatUtil'
defineProps<{
isInsufficientCredits?: boolean
}>()
const authStore = useFirebaseAuthStore()
const amount = ref<number>(9.99)
const didClickBuyNow = ref(false)
const loading = computed(() => authStore.loading)
const handleBlur = (e: any) => {
if (e.target.value) {
amount.value = parseFloat(e.target.value)
}
}
const handleInput = (e: any) => {
amount.value = e.value
}
const formattedBalance = computed(() => {
if (!authStore.balance) return '0.000'
return formatMetronomeCurrency(authStore.balance.amount_micros, 'usd')
})
const handleSeeDetails = async () => {
const response = await authStore.accessBillingPortal()
if (!response?.billing_portal_url) return
window.open(response.billing_portal_url, '_blank')
}
const handleBuyNow = async () => {
if (!amount.value) return
const response = await authStore.initiateCreditPurchase({
amount_micros: usdToMicros(amount.value),
currency: 'usd'
})
if (!response?.checkout_url) return
didClickBuyNow.value = true
// Go to Stripe checkout page
window.open(response.checkout_url, '_blank')
}
onBeforeUnmount(() => {
if (didClickBuyNow.value) {
// If clicked buy now, then returned back to the dialog and closed, fetch the balance
void authStore.fetchBalance()
}
})
</script>

View File

@@ -74,9 +74,7 @@
</div>
</div>
</div>
<div
class="w-80 border-l-0 border-surface-border absolute right-0 top-0 bottom-0 flex z-20"
>
<div class="w-80 border-l-0 absolute right-0 top-0 bottom-0 flex z-20">
<ContentDivider orientation="vertical" :width="0.2" />
<div class="flex-1 flex flex-col isolate">
<InfoPanel

View File

@@ -10,7 +10,7 @@
<ProgressSpinner class="w-8 h-8 mb-2" />
{{ $t('manager.loadingVersions') }}
</div>
<div v-else-if="allVersionOptions.length === 0" class="py-2">
<div v-else-if="versionOptions.length === 0" class="py-2">
<NoResultsPlaceholder
:title="$t('g.noResultsFound')"
:message="$t('manager.tryAgainLater')"
@@ -23,7 +23,7 @@
v-model="selectedVersion"
option-label="label"
option-value="value"
:options="allVersionOptions"
:options="versionOptions"
:highlight-on-select="false"
class="my-3 w-full max-h-[50vh] border-none shadow-none"
>
@@ -58,11 +58,11 @@
</template>
<script setup lang="ts">
import { useAsyncState, whenever } from '@vueuse/core'
import { whenever } from '@vueuse/core'
import Button from 'primevue/button'
import Listbox from 'primevue/listbox'
import ProgressSpinner from 'primevue/progressspinner'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import ContentDivider from '@/components/common/ContentDivider.vue'
@@ -119,47 +119,53 @@ const fetchVersions = async () => {
return (await registryService.getPackVersions(nodePack.id)) || []
}
const {
isLoading: isLoadingVersions,
state: versions,
execute: startFetchVersions
} = useAsyncState(fetchVersions, [])
const versionOptions = ref<
{
value: string
label: string
}[]
>([])
const specialOptions = computed(() => {
const options = [
const isLoadingVersions = ref(false)
const onNodePackChange = async () => {
isLoadingVersions.value = true
// Fetch versions from the registry
const versions = await fetchVersions()
const availableVersionOptions = versions
.map((version) => ({
value: version.version ?? '',
label: version.version ?? ''
}))
.filter((option) => option.value)
// Add Latest option
const defaultVersions = [
{
value: SelectedVersion.LATEST,
label: t('manager.latestVersion')
}
]
// Only include nightly option if there is a repo
// Add Nightly option if there is a non-empty `repository` field
if (nodePack.repository?.length) {
options.push({
defaultVersions.push({
value: SelectedVersion.NIGHTLY,
label: t('manager.nightlyVersion')
})
}
return options
})
const versionOptions = computed(() =>
versions.value.map((version) => ({
value: version.version,
label: version.version
}))
)
const allVersionOptions = computed(() => [
...specialOptions.value,
...versionOptions.value
])
versionOptions.value = [...defaultVersions, ...availableVersionOptions]
isLoadingVersions.value = false
}
whenever(
() => nodePack.id,
() => startFetchVersions(),
{ deep: true }
() => nodePack,
() => {
void onNodePackChange()
},
{ deep: true, immediate: true }
)
const handleSubmit = async () => {

View File

@@ -16,7 +16,7 @@
<em v-else-if="segment.type === 'italic'">{{ segment.text }}</em>
<code
v-else-if="segment.type === 'code'"
class="bg-surface-100 px-1 py-0.5 rounded text-xs"
class="px-1 py-0.5 rounded text-xs"
>{{ segment.text }}</code
>
<span v-else>{{ segment.text }}</span>

View File

@@ -4,7 +4,7 @@
<div
v-for="nodeDef in mappedNodeDefs"
:key="createNodeDefKey(nodeDef)"
class="border border-surface-border rounded-lg p-4"
class="border rounded-lg p-4"
>
<NodePreview :node-def="nodeDef" class="!text-[.625rem] !min-w-full" />
</div>

View File

@@ -10,9 +10,7 @@
zIndex: maxVisible - index
}"
>
<div
class="border border-surface-border bg-surface-card rounded-lg p-0.5"
>
<div class="border rounded-lg p-0.5">
<PackIcon :node-pack="pack" width="4.5rem" height="4.5rem" />
</div>
</div>

View File

@@ -0,0 +1,184 @@
<template>
<TabPanel value="Credits" class="credits-container h-full">
<div class="flex flex-col h-full">
<h2 class="text-2xl font-bold mb-2">
{{ $t('credits.credits') }}
</h2>
<Divider />
<div class="flex flex-col gap-2">
<h3 class="text-sm font-medium text-muted">
{{ $t('credits.yourCreditBalance') }}
</h3>
<div class="flex justify-between items-center">
<div class="flex items-center gap-1">
<Tag
severity="secondary"
icon="pi pi-dollar"
rounded
class="text-amber-400 p-1"
/>
<div class="text-3xl font-bold">{{ formattedBalance }}</div>
</div>
<ProgressSpinner
v-if="loading"
class="w-12 h-12"
style="--pc-spinner-color: #000"
/>
<Button
v-else
:label="$t('credits.purchaseCredits')"
:loading="loading"
@click="handlePurchaseCreditsClick"
/>
</div>
<div class="flex flex-row items-center">
<div v-if="formattedLastUpdateTime" class="text-xs text-muted">
{{ $t('credits.lastUpdated') }}: {{ formattedLastUpdateTime }}
</div>
<Button
icon="pi pi-refresh"
text
size="small"
severity="secondary"
@click="() => authStore.fetchBalance()"
/>
</div>
</div>
<div class="flex justify-between items-center mt-8">
<Button
:label="$t('credits.creditsHistory')"
text
severity="secondary"
icon="pi pi-arrow-up-right"
:loading="loading"
@click="handleCreditsHistoryClick"
/>
</div>
<template v-if="creditHistory.length > 0">
<div class="flex-grow">
<DataTable :value="creditHistory" :show-headers="false">
<Column field="title" :header="$t('g.name')">
<template #body="{ data }">
<div class="text-sm font-medium">{{ data.title }}</div>
<div class="text-xs text-muted">{{ data.timestamp }}</div>
</template>
</Column>
<Column field="amount" :header="$t('g.amount')">
<template #body="{ data }">
<div
:class="[
'text-base font-medium text-center',
data.isPositive ? 'text-sky-500' : 'text-red-400'
]"
>
{{ data.isPositive ? '+' : '-' }}${{
formatMetronomeCurrency(data.amount, 'usd')
}}
</div>
</template>
</Column>
</DataTable>
</div>
</template>
<Divider />
<div class="flex flex-row gap-2">
<Button
:label="$t('credits.faqs')"
text
severity="secondary"
icon="pi pi-question-circle"
@click="handleFaqClick"
/>
<Button
:label="$t('credits.messageSupport')"
text
severity="secondary"
icon="pi pi-comments"
@click="handleMessageSupport"
/>
</div>
</div>
</TabPanel>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Column from 'primevue/column'
import DataTable from 'primevue/datatable'
import Divider from 'primevue/divider'
import ProgressSpinner from 'primevue/progressspinner'
import TabPanel from 'primevue/tabpanel'
import Tag from 'primevue/tag'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useDialogService } from '@/services/dialogService'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { formatMetronomeCurrency } from '@/utils/formatUtil'
interface CreditHistoryItemData {
title: string
timestamp: string
amount: number
isPositive: boolean
}
const { t } = useI18n()
const dialogService = useDialogService()
const authStore = useFirebaseAuthStore()
const loading = computed(() => authStore.loading)
// Format balance from micros to dollars
const formattedBalance = computed(() => {
if (!authStore.balance) return '0.00'
return formatMetronomeCurrency(authStore.balance.amount_micros, 'usd')
})
const formattedLastUpdateTime = computed(() =>
authStore.lastBalanceUpdateTime
? authStore.lastBalanceUpdateTime.toLocaleString()
: ''
)
const handlePurchaseCreditsClick = () => {
dialogService.showTopUpCreditsDialog()
}
const handleCreditsHistoryClick = async () => {
const response = await authStore.accessBillingPortal()
if (!response) return
const { billing_portal_url } = response
if (billing_portal_url) {
window.open(billing_portal_url, '_blank')
}
}
const handleMessageSupport = () => {
dialogService.showIssueReportDialog({
title: t('credits.messageSupport'),
subtitle: t('issueReport.feedbackTitle'),
panelProps: {
errorType: 'BillingSupport',
defaultFields: ['SystemStats', 'Settings']
}
})
}
const handleFaqClick = () => {
window.open('https://drip-art.notion.site/api-nodes-faqs', '_blank')
}
// Fetch initial balance when panel is mounted
onMounted(() => {
void authStore.fetchBalance()
})
const creditHistory = ref<CreditHistoryItemData[]>([])
</script>

View File

@@ -0,0 +1,137 @@
<template>
<TabPanel value="User" class="user-settings-container h-full">
<div class="flex flex-col h-full">
<h2 class="text-xl font-bold mb-2">{{ $t('userSettings.title') }}</h2>
<Divider class="mb-3" />
<div v-if="user" class="flex flex-col gap-2">
<!-- User Avatar if available -->
<div v-if="user.photoURL" class="flex items-center gap-2">
<img
:src="user.photoURL"
:alt="user.displayName || ''"
class="w-8 h-8 rounded-full"
/>
</div>
<div class="flex flex-col gap-0.5">
<h3 class="font-medium">
{{ $t('userSettings.name') }}
</h3>
<div class="text-muted">
{{ user.displayName || $t('userSettings.notSet') }}
</div>
</div>
<div class="flex flex-col gap-0.5">
<h3 class="font-medium">
{{ $t('userSettings.email') }}
</h3>
<a :href="'mailto:' + user.email" class="hover:underline">
{{ user.email }}
</a>
</div>
<div class="flex flex-col gap-0.5">
<h3 class="font-medium">
{{ $t('userSettings.provider') }}
</h3>
<div class="text-muted flex items-center gap-1">
<i :class="providerIcon" />
{{ providerName }}
</div>
</div>
<ProgressSpinner
v-if="loading"
class="w-8 h-8 mt-4"
style="--pc-spinner-color: #000"
/>
<Button
v-else
class="mt-4 w-32"
severity="secondary"
:label="$t('auth.signOut.signOut')"
icon="pi pi-sign-out"
@click="handleSignOut"
/>
</div>
<!-- Login Section -->
<div v-else class="flex flex-col gap-4">
<p class="text-gray-600">
{{ $t('auth.login.title') }}
</p>
<Button
class="w-52"
severity="primary"
:loading="loading"
:label="$t('auth.login.signInOrSignUp')"
icon="pi pi-user"
@click="handleSignIn"
/>
</div>
</div>
</TabPanel>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Divider from 'primevue/divider'
import ProgressSpinner from 'primevue/progressspinner'
import TabPanel from 'primevue/tabpanel'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useDialogService } from '@/services/dialogService'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { useToastStore } from '@/stores/toastStore'
const toast = useToastStore()
const { t } = useI18n()
const authStore = useFirebaseAuthStore()
const dialogService = useDialogService()
const user = computed(() => authStore.currentUser)
const loading = computed(() => authStore.loading)
const providerName = computed(() => {
const providerId = user.value?.providerData[0]?.providerId
if (providerId?.includes('google')) {
return 'Google'
}
if (providerId?.includes('github')) {
return 'GitHub'
}
return providerId
})
const providerIcon = computed(() => {
const providerId = user.value?.providerData[0]?.providerId
if (providerId?.includes('google')) {
return 'pi pi-google'
}
if (providerId?.includes('github')) {
return 'pi pi-github'
}
return 'pi pi-user'
})
const handleSignOut = async () => {
await authStore.logout()
if (authStore.error) {
toast.addAlert(authStore.error)
} else {
toast.add({
severity: 'success',
summary: t('auth.signOut.success'),
detail: t('auth.signOut.successDetail'),
life: 5000
})
}
}
const handleSignIn = async () => {
await dialogService.showSignInDialog()
}
</script>

View File

@@ -57,7 +57,9 @@
</div>
<!-- Submit Button -->
<ProgressSpinner v-if="loading" class="w-8 h-8" />
<Button
v-else
type="submit"
:label="t('auth.login.loginButton')"
class="h-10 font-medium mt-4"
@@ -71,9 +73,15 @@ import { zodResolver } from '@primevue/forms/resolvers/zod'
import Button from 'primevue/button'
import InputText from 'primevue/inputtext'
import Password from 'primevue/password'
import ProgressSpinner from 'primevue/progressspinner'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { type SignInData, signInSchema } from '@/schemas/signInSchema'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
const authStore = useFirebaseAuthStore()
const loading = computed(() => authStore.loading)
const { t } = useI18n()

View File

@@ -73,6 +73,7 @@ watch(
updateWidgets()
}
)
}
},
{ immediate: true }
)
</script>

View File

@@ -19,7 +19,6 @@
<GraphCanvasMenu v-if="canvasMenuEnabled" class="pointer-events-auto" />
</template>
</LiteGraphCanvasSplitterOverlay>
<TitleEditor />
<GraphCanvasMenu v-if="!betaMenuEnabled && canvasMenuEnabled" />
<canvas
id="graph-canvas"
@@ -27,13 +26,20 @@
tabindex="1"
class="w-full h-full touch-none"
/>
<NodeSearchboxPopover />
<SelectionOverlay v-if="selectionToolboxEnabled">
<SelectionToolbox />
</SelectionOverlay>
<NodeTooltip v-if="tooltipEnabled" />
<NodeBadge />
<DomWidgets />
<NodeTooltip v-if="tooltipEnabled" />
<NodeSearchboxPopover />
<!-- Initialize components after comfyApp is ready. useAbsolutePosition requires
canvasStore.canvas to be initialized. -->
<template v-if="comfyAppReady">
<TitleEditor />
<SelectionOverlay v-if="selectionToolboxEnabled">
<SelectionToolbox />
</SelectionOverlay>
<DomWidgets />
</template>
</template>
<script setup lang="ts">
@@ -78,7 +84,9 @@ import { useSettingStore } from '@/stores/settingStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
const emit = defineEmits(['ready'])
const emit = defineEmits<{
ready: []
}>()
const canvasRef = ref<HTMLCanvasElement | null>(null)
const settingStore = useSettingStore()
const nodeDefStore = useNodeDefStore()

View File

@@ -16,7 +16,7 @@
import { LGraphGroup, LGraphNode, LiteGraph } from '@comfyorg/litegraph'
import type { LiteGraphCanvasEvent } from '@comfyorg/litegraph'
import { useEventListener } from '@vueuse/core'
import { ref, watch } from 'vue'
import { type CSSProperties, computed, ref, watch } from 'vue'
import EditableText from '@/components/common/EditableText.vue'
import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'
@@ -28,7 +28,12 @@ const settingStore = useSettingStore()
const showInput = ref(false)
const editedTitle = ref('')
const { style: inputStyle, updatePosition } = useAbsolutePosition()
const { style: inputPositionStyle, updatePosition } = useAbsolutePosition()
const inputFontStyle = ref<CSSProperties>({})
const inputStyle = computed<CSSProperties>(() => ({
...inputPositionStyle.value,
...inputFontStyle.value
}))
const titleEditorStore = useTitleEditorStore()
const canvasStore = useCanvasStore()
@@ -59,23 +64,19 @@ watch(
if (target instanceof LGraphGroup) {
const group = target
updatePosition(
{
pos: group.pos,
size: [group.size[0], group.titleHeight]
},
{ fontSize: `${group.font_size * scale}px` }
)
updatePosition({
pos: group.pos,
size: [group.size[0], group.titleHeight]
})
inputFontStyle.value = { fontSize: `${group.font_size * scale}px` }
} else if (target instanceof LGraphNode) {
const node = target
const [x, y] = node.getBounding()
updatePosition(
{
pos: [x, y],
size: [node.width, LiteGraph.NODE_TITLE_HEIGHT]
},
{ fontSize: `${12 * scale}px` }
)
updatePosition({
pos: [x, y],
size: [node.width, LiteGraph.NODE_TITLE_HEIGHT]
})
inputFontStyle.value = { fontSize: `${12 * scale}px` }
}
}
)

View File

@@ -37,13 +37,14 @@ const { widget, widgetState } = defineProps<{
}>()
const emit = defineEmits<{
(e: 'update:widgetValue', value: string | object): void
'update:widgetValue': [value: string | object]
}>()
const widgetElement = ref<HTMLElement | undefined>()
const { style: positionStyle, updatePositionWithTransform } =
useAbsolutePosition()
const { style: positionStyle, updatePosition } = useAbsolutePosition({
useTransform: true
})
const { style: clippingStyle, updateClipPath } = useDomClipping()
const style = computed<CSSProperties>(() => ({
...positionStyle.value,
@@ -94,7 +95,7 @@ const updateDomClipping = () => {
watch(
() => widgetState,
(newState) => {
updatePositionWithTransform(newState)
updatePosition(newState)
if (enableDomClipping.value) {
updateDomClipping()
}

View File

@@ -37,7 +37,8 @@
import {
CUDA_TORCH_URL,
NIGHTLY_CPU_TORCH_URL,
TorchDeviceType
TorchDeviceType,
TorchMirrorUrl
} from '@comfyorg/comfyui-electron-types'
import Divider from 'primevue/divider'
import Panel from 'primevue/panel'
@@ -46,6 +47,7 @@ import { ModelRef, computed, onMounted, ref } from 'vue'
import MirrorItem from '@/components/install/mirror/MirrorItem.vue'
import { PYPI_MIRROR, PYTHON_MIRROR, UVMirror } from '@/constants/uvMirrors'
import { t } from '@/i18n'
import { electronAPI } from '@/utils/envUtil'
import { isInChina } from '@/utils/networkUtil'
import { ValidationState, mergeValidationStates } from '@/utils/validationUtil'
@@ -55,6 +57,17 @@ const pythonMirror = defineModel<string>('pythonMirror', { required: true })
const pypiMirror = defineModel<string>('pypiMirror', { required: true })
const torchMirror = defineModel<string>('torchMirror', { required: true })
const isBlackwellArchitecture = ref(false)
const requiresNightlyPytorch = async (): Promise<boolean> => {
try {
return await electronAPI().isBlackwell()
} catch (error) {
console.error('Failed to detect Blackwell architecture:', error)
return false
}
}
const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
const settingId = 'Comfy-Desktop.UV.TorchInstallMirror'
switch (device) {
@@ -65,6 +78,13 @@ const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
fallbackMirror: NIGHTLY_CPU_TORCH_URL
}
case 'nvidia':
if (isBlackwellArchitecture.value) {
return {
settingId,
mirror: TorchMirrorUrl.NightlyCuda,
fallbackMirror: TorchMirrorUrl.NightlyCuda
}
}
return {
settingId,
mirror: CUDA_TORCH_URL,
@@ -83,6 +103,7 @@ const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
const userIsInChina = ref(false)
onMounted(async () => {
userIsInChina.value = await isInChina()
isBlackwellArchitecture.value = await requiresNightlyPytorch()
})
const useFallbackMirror = (mirror: UVMirror) => ({

View File

@@ -21,9 +21,9 @@
<Slider
v-model="lightIntensity"
class="w-full"
:min="1"
:max="20"
:step="1"
:min="lightIntensityMinimum"
:max="lightIntensityMaximum"
:step="lightAdjustmentIncrement"
@change="updateLightIntensity"
/>
</div>
@@ -38,6 +38,7 @@ import Slider from 'primevue/slider'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { t } from '@/i18n'
import { useSettingStore } from '@/stores/settingStore'
const vTooltip = Tooltip
@@ -54,6 +55,16 @@ const lightIntensity = ref(props.lightIntensity)
const showLightIntensityButton = ref(props.showLightIntensityButton)
const showLightIntensity = ref(false)
const lightIntensityMaximum = useSettingStore().get(
'Comfy.Load3D.LightIntensityMaximum'
)
const lightIntensityMinimum = useSettingStore().get(
'Comfy.Load3D.LightIntensityMinimum'
)
const lightAdjustmentIncrement = useSettingStore().get(
'Comfy.Load3D.LightAdjustmentIncrement'
)
watch(
() => props.lightIntensity,
(newValue) => {

View File

@@ -33,43 +33,41 @@
</template>
<script setup lang="ts">
import { LiteGraph } from '@comfyorg/litegraph'
import type {
ConnectingLink,
LiteGraphCanvasEvent,
Vector2
import {
LGraphNode,
LiteGraph,
LiteGraphCanvasEvent
} from '@comfyorg/litegraph'
import type { OriginalEvent } from '@comfyorg/litegraph/dist/types/events'
import { Point } from '@comfyorg/litegraph/dist/interfaces'
import type { CanvasPointerEvent } from '@comfyorg/litegraph/dist/types/events'
import { useEventListener } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import Dialog from 'primevue/dialog'
import { computed, ref, toRaw, watchEffect } from 'vue'
import { computed, ref, toRaw, watch, watchEffect } from 'vue'
import { useLitegraphService } from '@/services/litegraphService'
import { useCanvasStore } from '@/stores/graphStore'
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
import { useSettingStore } from '@/stores/settingStore'
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
import { ConnectingLinkImpl } from '@/types/litegraphTypes'
import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'
import { FuseFilterWithValue } from '@/utils/fuseUtil'
import NodeSearchBox from './NodeSearchBox.vue'
let triggerEvent: CanvasPointerEvent | null = null
let listenerController: AbortController | null = null
let disconnectOnReset = false
const settingStore = useSettingStore()
const litegraphService = useLitegraphService()
const { visible } = storeToRefs(useSearchBoxStore())
const dismissable = ref(true)
const triggerEvent = ref<LiteGraphCanvasEvent | null>(null)
const getNewNodeLocation = (): Vector2 => {
if (!triggerEvent.value) {
return litegraphService.getCanvasCenter()
}
const originalEvent = (triggerEvent.value.detail as OriginalEvent)
.originalEvent
return [originalEvent.canvasX, originalEvent.canvasY]
const getNewNodeLocation = (): Point => {
return triggerEvent
? [triggerEvent.canvasX, triggerEvent.canvasY]
: litegraphService.getCanvasCenter()
}
const nodeFilters = ref<FuseFilterWithValue<ComfyNodeDefImpl, string>[]>([])
const addFilter = (filter: FuseFilterWithValue<ComfyNodeDefImpl, string>) => {
@@ -88,35 +86,30 @@ const clearFilters = () => {
const closeDialog = () => {
visible.value = false
}
const canvasStore = useCanvasStore()
const addNode = (nodeDef: ComfyNodeDefImpl) => {
if (!triggerEvent) {
console.warn('The trigger event was undefined when addNode was called.')
return
}
disconnectOnReset = false
const node = litegraphService.addNodeOnGraph(nodeDef, {
pos: getNewNodeLocation()
})
const eventDetail = triggerEvent.value?.detail
if (eventDetail && eventDetail.subType === 'empty-release') {
// @ts-expect-error fixme ts strict error
eventDetail.linkReleaseContext.links.forEach((link: ConnectingLink) => {
ConnectingLinkImpl.createFromPlainObject(link).connectTo(node)
})
}
canvasStore.getCanvas().linkConnector.connectToNode(node, triggerEvent)
// TODO: This is not robust timing-wise.
// PrimeVue complains about the dialog being closed before the event selecting
// item is fully processed.
window.setTimeout(() => {
closeDialog()
}, 100)
window.requestAnimationFrame(closeDialog)
}
const newSearchBoxEnabled = computed(
() => settingStore.get('Comfy.NodeSearchBoxImpl') === 'default'
)
const showSearchBox = (e: LiteGraphCanvasEvent) => {
const detail = e.detail as OriginalEvent
const showSearchBox = (e: CanvasPointerEvent) => {
if (newSearchBoxEnabled.value) {
if (detail.originalEvent?.pointerType === 'touch') {
if (e.pointerType === 'touch') {
setTimeout(() => {
showNewSearchBox(e)
}, 128)
@@ -124,26 +117,23 @@ const showSearchBox = (e: LiteGraphCanvasEvent) => {
showNewSearchBox(e)
}
} else {
// @ts-expect-error fixme ts strict error
canvasStore.canvas.showSearchBox(detail.originalEvent)
canvasStore.getCanvas().showSearchBox(e)
}
}
const getFirstLink = () =>
canvasStore.getCanvas().linkConnector.renderLinks.at(0)
const nodeDefStore = useNodeDefStore()
const showNewSearchBox = (e: LiteGraphCanvasEvent) => {
if (e.detail.subType === 'empty-release') {
const links = e.detail.linkReleaseContext.links
if (links.length === 0) {
console.warn('Empty release with no links! This should never happen')
return
}
const firstLink = ConnectingLinkImpl.createFromPlainObject(links[0])
const showNewSearchBox = (e: CanvasPointerEvent) => {
const firstLink = getFirstLink()
if (firstLink) {
const filter =
firstLink.releaseSlotType === 'input'
firstLink.toType === 'input'
? nodeDefStore.nodeSearchService.inputTypeFilter
: nodeDefStore.nodeSearchService.outputTypeFilter
const dataType = firstLink.type?.toString() ?? ''
const dataType = firstLink.fromSlot.type?.toString() ?? ''
addFilter({
filterDef: filter,
value: dataType
@@ -151,7 +141,7 @@ const showNewSearchBox = (e: LiteGraphCanvasEvent) => {
}
visible.value = true
triggerEvent.value = e
triggerEvent = e
// Prevent the dialog from being dismissed immediately
dismissable.value = false
@@ -160,88 +150,121 @@ const showNewSearchBox = (e: LiteGraphCanvasEvent) => {
}, 300)
}
const showContextMenu = (e: LiteGraphCanvasEvent) => {
if (e.detail.subType !== 'empty-release') {
return
}
const showContextMenu = (e: CanvasPointerEvent) => {
const firstLink = getFirstLink()
if (!firstLink) return
const links = e.detail.linkReleaseContext.links
if (links.length === 0) {
console.warn('Empty release with no links! This should never happen')
return
}
const firstLink = ConnectingLinkImpl.createFromPlainObject(links[0])
const mouseEvent = e.detail.originalEvent
const { node, fromSlot, toType } = firstLink
const commonOptions = {
e: mouseEvent,
e,
allow_searchbox: true,
showSearchBox: () => showSearchBox(e)
}
const connectionOptions = firstLink.output
? {
nodeFrom: firstLink.node,
slotFrom: firstLink.output,
afterRerouteId: firstLink.afterRerouteId
}
: {
nodeTo: firstLink.node,
slotTo: firstLink.input,
afterRerouteId: firstLink.afterRerouteId
}
// @ts-expect-error fixme ts strict error
canvasStore.canvas.showConnectionMenu({
const connectionOptions =
toType === 'input'
? { nodeFrom: node, slotFrom: fromSlot }
: { nodeTo: node, slotTo: fromSlot }
const canvas = canvasStore.getCanvas()
const menu = canvas.showConnectionMenu({
...connectionOptions,
...commonOptions
})
if (!menu) {
console.warn('No menu was returned from showConnectionMenu')
return
}
triggerEvent = e
listenerController = new AbortController()
const { signal } = listenerController
const options = { once: true, signal }
// Connect the node after it is created via context menu
useEventListener(
canvas.canvas,
'connect-new-default-node',
(createEvent) => {
if (!(createEvent instanceof CustomEvent))
throw new Error('Invalid event')
const node: unknown = createEvent.detail?.node
if (!(node instanceof LGraphNode)) throw new Error('Invalid node')
disconnectOnReset = false
createEvent.preventDefault()
canvas.linkConnector.connectToNode(node, e)
},
options
)
// Reset when the context menu is closed
useEventListener(menu.controller.signal, 'abort', reset, options)
}
// Disable litegraph's default behavior of release link and search box.
const canvasStore = useCanvasStore()
watchEffect(() => {
if (canvasStore.canvas) {
LiteGraph.release_link_on_empty_shows_menu = false
canvasStore.canvas.allow_searchbox = false
}
const { canvas } = canvasStore
if (!canvas) return
LiteGraph.release_link_on_empty_shows_menu = false
canvas.allow_searchbox = false
useEventListener(
canvas.linkConnector.events,
'dropped-on-canvas',
handleDroppedOnCanvas
)
})
const canvasEventHandler = (e: LiteGraphCanvasEvent) => {
if (e.detail.subType === 'empty-double-click') {
showSearchBox(e)
} else if (e.detail.subType === 'empty-release') {
handleCanvasEmptyRelease(e)
showSearchBox(e.detail.originalEvent)
} else if (e.detail.subType === 'group-double-click') {
const group = e.detail.group
const [_, y] = group.pos
const relativeY = e.detail.originalEvent.canvasY - y
// Show search box if the click is NOT on the title bar
if (relativeY > group.titleHeight) {
showSearchBox(e)
showSearchBox(e.detail.originalEvent)
}
}
}
const linkReleaseAction = computed(() => {
return settingStore.get('Comfy.LinkRelease.Action')
})
const linkReleaseAction = computed(() =>
settingStore.get('Comfy.LinkRelease.Action')
)
const linkReleaseActionShift = computed(() => {
return settingStore.get('Comfy.LinkRelease.ActionShift')
})
const linkReleaseActionShift = computed(() =>
settingStore.get('Comfy.LinkRelease.ActionShift')
)
const handleCanvasEmptyRelease = (e: LiteGraphCanvasEvent) => {
const detail = e.detail as OriginalEvent
const shiftPressed = detail.originalEvent.shiftKey
// Prevent normal LinkConnector reset (called by CanvasPointer.finally)
const preventDefault = (e: Event) => e.preventDefault()
const cancelNextReset = (e: CustomEvent<CanvasPointerEvent>) => {
e.preventDefault()
const action = shiftPressed
const canvas = canvasStore.getCanvas()
canvas._highlight_pos = [e.detail.canvasX, e.detail.canvasY]
useEventListener(canvas.linkConnector.events, 'reset', preventDefault, {
once: true
})
}
const handleDroppedOnCanvas = (e: CustomEvent<CanvasPointerEvent>) => {
disconnectOnReset = true
const action = e.detail.shiftKey
? linkReleaseActionShift.value
: linkReleaseAction.value
switch (action) {
case LinkReleaseTriggerAction.SEARCH_BOX:
showSearchBox(e)
cancelNextReset(e)
showSearchBox(e.detail)
break
case LinkReleaseTriggerAction.CONTEXT_MENU:
showContextMenu(e)
cancelNextReset(e)
showContextMenu(e.detail)
break
case LinkReleaseTriggerAction.NO_ACTION:
default:
@@ -249,6 +272,26 @@ const handleCanvasEmptyRelease = (e: LiteGraphCanvasEvent) => {
}
}
// Resets litegraph state
const reset = () => {
listenerController?.abort()
listenerController = null
triggerEvent = null
const canvas = canvasStore.getCanvas()
canvas.linkConnector.events.removeEventListener('reset', preventDefault)
if (disconnectOnReset) canvas.linkConnector.disconnectLinks()
canvas.linkConnector.reset()
canvas._highlight_pos = undefined
canvas.setDirty(true, true)
}
// Reset connecting links when the search box is closed
watch(visible, () => {
if (!visible.value) reset()
})
useEventListener(document, 'litegraph:canvas', canvasEventHandler)
</script>

View File

@@ -52,21 +52,15 @@
<template #content>
<div class="flex items-center px-4 py-3">
<div class="flex-1">
<h3
class="line-clamp-1 text-lg font-normal text-surface-900 dark:text-surface-100"
:title="title"
>
<h3 class="line-clamp-1 text-lg font-normal" :title="title">
{{ title }}
</h3>
<p
class="line-clamp-2 text-sm text-surface-600 dark:text text-muted"
:title="description"
>
<p class="line-clamp-2 text-sm text-muted" :title="description">
{{ description }}
</p>
</div>
<div
class="flex md:hidden xl:flex items-center justify-center ml-4 w-10 h-10 rounded-full bg-surface-100"
class="flex md:hidden xl:flex items-center justify-center ml-4 w-10 h-10 rounded-full"
>
<i class="pi pi-angle-right text-2xl" />
</div>
@@ -123,12 +117,13 @@ const overlayThumbnailSrc = computed(() =>
)
const title = computed(() => {
const fallback = template.title ?? template.name ?? `${sourceModule} Template`
return sourceModule === 'default'
? st(
`templateWorkflows.template.${normalizeI18nKey(categoryTitle)}.${normalizeI18nKey(template.name)}`,
template.name
fallback
)
: template.name ?? `${sourceModule} Template`
: fallback
})
const description = computed(() => template.description.replace(/[-_]/g, ' '))

View File

@@ -10,11 +10,8 @@
>
<slot />
</div>
<div
v-else
class="w-full h-full flex items-center justify-center bg-surface-card"
>
<i class="pi pi-file text-4xl text-surface-600" />
<div v-else class="w-full h-full flex items-center justify-center">
<i class="pi pi-file text-4xl" />
</div>
</div>
</template>

View File

@@ -23,6 +23,23 @@
@click="workspaceState.focusMode = true"
@contextmenu="showNativeSystemMenu"
/>
<Button
v-if="isAuthenticated"
v-tooltip="{ value: $t('userSettings.title'), showDelay: 300 }"
class="flex-shrink-0 user-profile-button"
severity="secondary"
text
:aria-label="$t('userSettings.title')"
@click="openUserSettings"
>
<template #icon>
<div
class="w-6 h-6 rounded-full bg-neutral-100 dark:bg-neutral-700 flex items-center justify-center"
>
<i class="pi pi-user text-sm" />
</div>
</template>
</Button>
<div
v-show="menuSetting !== 'Bottom'"
class="window-actions-spacer flex-shrink-0"
@@ -46,6 +63,8 @@ import BottomPanelToggleButton from '@/components/topbar/BottomPanelToggleButton
import CommandMenubar from '@/components/topbar/CommandMenubar.vue'
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { useSettingStore } from '@/stores/settingStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import {
@@ -57,6 +76,10 @@ import {
const workspaceState = useWorkspaceStore()
const settingStore = useSettingStore()
const authStore = useFirebaseAuthStore()
const dialogService = useDialogService()
const isAuthenticated = computed(() => authStore.isAuthenticated)
const workflowTabsPosition = computed(() =>
settingStore.get('Comfy.Workflow.WorkflowTabsPosition')
)
@@ -66,6 +89,10 @@ const showTopMenu = computed(
() => betaMenuEnabled.value && !workspaceState.focusMode
)
const openUserSettings = () => {
dialogService.showSettingsDialog('user')
}
const menuRight = ref<HTMLDivElement | null>(null)
// Menu-right holds legacy topbar elements attached by custom scripts
onMounted(() => {

View File

@@ -1,7 +1,7 @@
import type { Size, Vector2 } from '@comfyorg/litegraph'
import { CSSProperties, ref } from 'vue'
import { CSSProperties, computed, ref } from 'vue'
import { app } from '@/scripts/app'
import { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
import { useCanvasStore } from '@/stores/graphStore'
export interface PositionConfig {
@@ -13,70 +13,56 @@ export interface PositionConfig {
scale?: number
}
export function useAbsolutePosition() {
export function useAbsolutePosition(options: { useTransform?: boolean } = {}) {
const { useTransform = false } = options
const canvasStore = useCanvasStore()
const style = ref<CSSProperties>({
position: 'fixed',
left: '0px',
top: '0px',
width: '0px',
height: '0px'
const lgCanvas = canvasStore.getCanvas()
const { canvasPosToClientPos } = useCanvasPositionConversion(
lgCanvas.canvas,
lgCanvas
)
const position = ref<PositionConfig>({
pos: [0, 0],
size: [0, 0]
})
const style = computed<CSSProperties>(() => {
const { pos, size, scale = lgCanvas.ds.scale } = position.value
const [left, top] = canvasPosToClientPos(pos)
const [width, height] = size
return useTransform
? {
position: 'fixed',
transformOrigin: '0 0',
transform: `scale(${scale})`,
left: `${left}px`,
top: `${top}px`,
width: `${width}px`,
height: `${height}px`
}
: {
position: 'fixed',
left: `${left}px`,
top: `${top}px`,
width: `${width * scale}px`,
height: `${height * scale}px`
}
})
/**
* Update the position of the element on the litegraph canvas.
*
* @param config
* @param extraStyle
*/
const updatePosition = (
config: PositionConfig,
extraStyle?: CSSProperties
) => {
const { pos, size, scale = canvasStore.canvas?.ds?.scale ?? 1 } = config
const [left, top] = app.canvasPosToClientPos(pos)
const [width, height] = size
style.value = {
...style.value,
left: `${left}px`,
top: `${top}px`,
width: `${width * scale}px`,
height: `${height * scale}px`,
...extraStyle
}
}
/**
* Update the position and size of the element on the litegraph canvas,
* with CSS transform scaling applied.
*
* @param config
* @param extraStyle
*/
const updatePositionWithTransform = (
config: PositionConfig,
extraStyle?: CSSProperties
) => {
const { pos, size, scale = canvasStore.canvas?.ds?.scale ?? 1 } = config
const [left, top] = app.canvasPosToClientPos(pos)
const [width, height] = size
style.value = {
...style.value,
transformOrigin: '0 0',
transform: `scale(${scale})`,
left: `${left}px`,
top: `${top}px`,
width: `${width}px`,
height: `${height}px`,
...extraStyle
}
const updatePosition = (config: PositionConfig) => {
position.value = config
}
return {
style,
updatePosition,
updatePositionWithTransform
updatePosition
}
}

View File

@@ -16,8 +16,8 @@ export const useCanvasPositionConversion = (
const clientPosToCanvasPos = (pos: Vector2): Vector2 => {
const { offset, scale } = lgCanvas.ds
return [
(pos[0] - left.value) / scale + offset[0],
(pos[1] - top.value) / scale + offset[1]
(pos[0] - left.value) / scale - offset[0],
(pos[1] - top.value) / scale - offset[1]
]
}

View File

@@ -3,6 +3,7 @@ import type { IWidget } from '@comfyorg/litegraph/dist/types/widgets'
import { ANIM_PREVIEW_WIDGET } from '@/scripts/app'
import { createImageHost } from '@/scripts/ui/imagePreview'
import { fitDimensionsToNodeWidth } from '@/utils/imageUtil'
/**
* Composable for handling animated image previews in nodes
@@ -42,6 +43,16 @@ export function useNodeAnimatedImage() {
widget.serialize = false
widget.serializeValue = () => undefined
widget.options.host.updateImages(node.imgs)
widget.computeLayoutSize = () => {
const img = widget.options.host.getCurrentImage()
if (!img) return { minHeight: 0, minWidth: 0 }
return fitDimensionsToNodeWidth(
img.naturalWidth,
img.naturalHeight,
node.size?.[0] || 0
)
}
}
}

View File

@@ -1,6 +1,7 @@
import type { LGraphNode } from '@comfyorg/litegraph'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import { fitDimensionsToNodeWidth } from '@/utils/imageUtil'
const VIDEO_WIDGET_NAME = 'video-preview'
const VIDEO_DEFAULT_OPTIONS = {
@@ -131,12 +132,15 @@ export const useNodeVideo = (node: LGraphNode) => {
let minWidth = DEFAULT_VIDEO_SIZE
const setMinDimensions = (video: HTMLVideoElement) => {
const intrinsicAspectRatio = video.videoWidth / video.videoHeight
if (!intrinsicAspectRatio || isNaN(intrinsicAspectRatio)) return
const { minHeight: calculatedHeight, minWidth: calculatedWidth } =
fitDimensionsToNodeWidth(
video.videoWidth,
video.videoHeight,
node.size?.[0] || DEFAULT_VIDEO_SIZE
)
// Set min. height s.t. video spans node's x-axis while maintaining aspect ratio
minWidth = node.size?.[0] || DEFAULT_VIDEO_SIZE
minHeight = Math.max(minWidth / intrinsicAspectRatio, 64)
minWidth = calculatedWidth
minHeight = calculatedHeight
}
const loadElement = (url: string): Promise<HTMLVideoElement | null> =>

View File

@@ -5,6 +5,8 @@ import { useNodePacks } from '@/composables/nodePack/useNodePacks'
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
import { app } from '@/scripts/app'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import { SelectedVersion, UseNodePacksOptions } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
@@ -22,6 +24,8 @@ const CORE_NODES_PACK_NAME = 'comfy-core'
* associated node packs from the registry
*/
export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
const nodeDefStore = useNodeDefStore()
const systemStatsStore = useSystemStatsStore()
const { search } = useComfyRegistryStore()
const workflowPacks = ref<WorkflowPack[]>([])
@@ -36,6 +40,13 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
return undefined
}
/**
* Clean the version string to be used in the registry search.
* Removes the leading 'v' and trims whitespace and line terminators.
*/
const cleanVersionString = (version: string) =>
version.replace(/^v/, '').trim()
/**
* Infer the pack for a node by searching the registry for packs that have nodes
* with the same name.
@@ -44,6 +55,22 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
node: LGraphNode
): Promise<WorkflowPack | undefined> => {
const nodeName = node.type
// Check if node is a core node
const nodeDef = nodeDefStore.nodeDefsByName[nodeName]
if (nodeDef?.nodeSource.type === 'core') {
if (!systemStatsStore.systemStats) {
await systemStatsStore.fetchSystemStats()
}
return {
id: CORE_NODES_PACK_NAME,
version:
systemStatsStore.systemStats?.system?.comfyui_version ??
SelectedVersion.NIGHTLY
}
}
// Search the registry for non-core nodes
const searchResult = await search.call({
comfy_node_search: nodeName,
limit: 1
@@ -70,7 +97,9 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
if (packId === CORE_NODES_PACK_NAME) return undefined
const version =
typeof node.properties.ver === 'string' ? node.properties.ver : undefined
typeof node.properties.ver === 'string'
? cleanVersionString(node.properties.ver)
: undefined
return {
id: packId,

View File

@@ -0,0 +1,122 @@
import { computed, ref, watch } from 'vue'
import { st } from '@/i18n'
import {
SettingTreeNode,
getSettingInfo,
useSettingStore
} from '@/stores/settingStore'
import { ISettingGroup, SettingParams } from '@/types/settingTypes'
import { normalizeI18nKey } from '@/utils/formatUtil'
export function useSettingSearch() {
const settingStore = useSettingStore()
const searchQuery = ref<string>('')
const filteredSettingIds = ref<string[]>([])
const searchInProgress = ref<boolean>(false)
watch(searchQuery, () => (searchInProgress.value = true))
/**
* Settings categories that contains at least one setting in search results.
*/
const searchResultsCategories = computed<Set<string>>(() => {
return new Set(
filteredSettingIds.value.map(
(id) => getSettingInfo(settingStore.settingsById[id]).category
)
)
})
/**
* Check if the search query is empty
*/
const queryIsEmpty = computed(() => searchQuery.value.length === 0)
/**
* Check if we're in search mode
*/
const inSearch = computed(
() => !queryIsEmpty.value && !searchInProgress.value
)
/**
* Handle search functionality
*/
const handleSearch = (query: string) => {
if (!query) {
filteredSettingIds.value = []
return
}
const queryLower = query.toLocaleLowerCase()
const allSettings = Object.values(settingStore.settingsById)
const filteredSettings = allSettings.filter((setting) => {
const idLower = setting.id.toLowerCase()
const nameLower = setting.name.toLowerCase()
const translatedName = st(
`settings.${normalizeI18nKey(setting.id)}.name`,
setting.name
).toLocaleLowerCase()
const info = getSettingInfo(setting)
const translatedCategory = st(
`settingsCategories.${normalizeI18nKey(info.category)}`,
info.category
).toLocaleLowerCase()
const translatedSubCategory = st(
`settingsCategories.${normalizeI18nKey(info.subCategory)}`,
info.subCategory
).toLocaleLowerCase()
return (
idLower.includes(queryLower) ||
nameLower.includes(queryLower) ||
translatedName.includes(queryLower) ||
translatedCategory.includes(queryLower) ||
translatedSubCategory.includes(queryLower)
)
})
filteredSettingIds.value = filteredSettings.map((x) => x.id)
searchInProgress.value = false
}
/**
* Get search results grouped by category
*/
const getSearchResults = (
activeCategory: SettingTreeNode | null
): ISettingGroup[] => {
const groupedSettings: { [key: string]: SettingParams[] } = {}
filteredSettingIds.value.forEach((id) => {
const setting = settingStore.settingsById[id]
const info = getSettingInfo(setting)
const groupLabel = info.subCategory
if (activeCategory === null || activeCategory.label === info.category) {
if (!groupedSettings[groupLabel]) {
groupedSettings[groupLabel] = []
}
groupedSettings[groupLabel].push(setting)
}
})
return Object.entries(groupedSettings).map(([label, settings]) => ({
label,
settings
}))
}
return {
searchQuery,
filteredSettingIds,
searchInProgress,
searchResultsCategories,
queryIsEmpty,
inSearch,
handleSearch,
getSearchResults
}
}

View File

@@ -0,0 +1,155 @@
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { SettingTreeNode, useSettingStore } from '@/stores/settingStore'
import type { SettingParams } from '@/types/settingTypes'
import { isElectron } from '@/utils/envUtil'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { buildTree } from '@/utils/treeUtil'
export function useSettingUI(
defaultPanel?:
| 'about'
| 'keybinding'
| 'extension'
| 'server-config'
| 'user'
| 'credits'
) {
const { t } = useI18n()
const firebaseAuthStore = useFirebaseAuthStore()
const settingStore = useSettingStore()
const activeCategory = ref<SettingTreeNode | null>(null)
const settingRoot = computed<SettingTreeNode>(() => {
const root = buildTree(
Object.values(settingStore.settingsById).filter(
(setting: SettingParams) => setting.type !== 'hidden'
),
(setting: SettingParams) => setting.category || setting.id.split('.')
)
const floatingSettings = (root.children ?? []).filter((node) => node.leaf)
if (floatingSettings.length) {
root.children = (root.children ?? []).filter((node) => !node.leaf)
root.children.push({
key: 'Other',
label: 'Other',
leaf: false,
children: floatingSettings
})
}
return root
})
const settingCategories = computed<SettingTreeNode[]>(
() => settingRoot.value.children ?? []
)
// Define panel nodes
const aboutPanelNode: SettingTreeNode = {
key: 'about',
label: 'About',
children: []
}
const creditsPanelNode: SettingTreeNode = {
key: 'credits',
label: 'Credits',
children: []
}
const userPanelNode: SettingTreeNode = {
key: 'user',
label: 'User',
children: []
}
const keybindingPanelNode: SettingTreeNode = {
key: 'keybinding',
label: 'Keybinding',
children: []
}
const extensionPanelNode: SettingTreeNode = {
key: 'extension',
label: 'Extension',
children: []
}
const serverConfigPanelNode: SettingTreeNode = {
key: 'server-config',
label: 'Server-Config',
children: []
}
/**
* Server config panel is only available in Electron
*/
const serverConfigPanelNodeList = computed<SettingTreeNode[]>(() => {
return isElectron() ? [serverConfigPanelNode] : []
})
/**
* The default category to show when the dialog is opened.
*/
const defaultCategory = computed<SettingTreeNode>(() => {
if (!defaultPanel) return settingCategories.value[0]
// Search through all groups in groupedMenuTreeNodes
for (const group of groupedMenuTreeNodes.value) {
const found = group.children?.find((node) => node.key === defaultPanel)
if (found) return found
}
return settingCategories.value[0]
})
const translateCategory = (node: SettingTreeNode) => ({
...node,
translatedLabel: t(
`settingsCategories.${normalizeI18nKey(node.label)}`,
node.label
)
})
const groupedMenuTreeNodes = computed<SettingTreeNode[]>(() => [
// Account settings - only show credits when user is authenticated
{
key: 'account',
label: 'Account',
children: [
userPanelNode,
...(firebaseAuthStore.isAuthenticated ? [creditsPanelNode] : [])
].map(translateCategory)
},
// Normal settings stored in the settingStore
{
key: 'settings',
label: 'Application Settings',
children: settingCategories.value.map(translateCategory)
},
// Special settings such as about, keybinding, extension, server-config
{
key: 'specialSettings',
label: 'Special Settings',
children: [
keybindingPanelNode,
extensionPanelNode,
aboutPanelNode,
...serverConfigPanelNodeList.value
].map(translateCategory)
}
])
onMounted(() => {
activeCategory.value = defaultCategory.value
})
return {
activeCategory,
defaultCategory,
groupedMenuTreeNodes,
settingCategories
}
}

View File

@@ -29,12 +29,10 @@ export const useCanvasDrop = (canvasRef: Ref<HTMLCanvasElement>) => {
const node = dndData.data as RenderedTreeExplorerNode
if (node.data instanceof ComfyNodeDefImpl) {
const nodeDef = node.data
// Add an offset on x to make sure after adding the node, the cursor
const pos = comfyApp.clientPosToCanvasPos([loc.clientX, loc.clientY])
// Add an offset on y to make sure after adding the node, the cursor
// is on the node (top left corner)
const pos = comfyApp.clientPosToCanvasPos([
loc.clientX,
loc.clientY + LiteGraph.NODE_TITLE_HEIGHT
])
pos[1] += LiteGraph.NODE_TITLE_HEIGHT
litegraphService.addNodeOnGraph(nodeDef, { pos })
} else if (node.data instanceof ComfyModelDef) {
const model = node.data

View File

@@ -616,6 +616,15 @@ export function useCoreCommands(): ComfyCommand[] {
function: () => {
dialogService.showManagerProgressDialog()
}
},
{
id: 'Comfy.User.OpenSignInDialog',
icon: 'pi pi-user',
label: 'Open Sign In Dialog',
versionAdded: '1.17.6',
function: async () => {
await dialogService.showSignInDialog()
}
}
]

View File

@@ -5,6 +5,7 @@ export function useErrorHandling() {
const toast = useToastStore()
const toastErrorHandler = (error: any) => {
console.error(error)
toast.add({
severity: 'error',
summary: t('g.error'),

View File

@@ -38,6 +38,15 @@ export const usePaste = () => {
}
useEventListener(document, 'paste', async (e) => {
const isTargetInGraph =
e.target instanceof Element &&
(e.target.classList.contains('litegraph') ||
e.target.classList.contains('graph-canvas-container') ||
e.target.id === 'graph-canvas')
// If the target is not in the graph, we don't want to handle the paste event
if (!isTargetInGraph) return
// ctrl+shift+v is used to paste nodes with connections
// this is handled by litegraph
if (workspaceStore.shiftDown) return

View File

@@ -18,7 +18,6 @@ import {
type ComfyWidgetConstructorV2,
addValueControlWidgets
} from '@/scripts/widgets'
import { generateUUID } from '@/utils/formatUtil'
import { useRemoteWidget } from './useRemoteWidget'
@@ -32,7 +31,6 @@ const getDefaultValue = (inputSpec: ComboInputSpec) => {
const addMultiSelectWidget = (node: LGraphNode, inputSpec: ComboInputSpec) => {
const widgetValue = ref<string[]>([])
const widget = new ComponentWidgetImpl({
id: generateUUID(),
node,
name: inputSpec.name,
component: MultiSelectWidget,

View File

@@ -37,6 +37,7 @@ export const useImageUploadWidget = () => {
const { imageInputName, allow_batch, image_folder = 'input' } = inputOptions
const nodeOutputStore = useNodeOutputStore()
const isAnimated = !!inputOptions.animated_image_upload
const isVideo = !!inputOptions.video_upload
const accept = isVideo ? ACCEPTED_VIDEO_TYPES : ACCEPTED_IMAGE_TYPES
const { showPreview } = isVideo ? useNodeVideo(node) : useNodeImage(node)
@@ -92,7 +93,9 @@ export const useImageUploadWidget = () => {
// Add our own callback to the combo widget to render an image when it changes
fileComboWidget.callback = function () {
nodeOutputStore.setNodeOutputs(node, fileComboWidget.value)
nodeOutputStore.setNodeOutputs(node, fileComboWidget.value, {
isAnimated
})
node.graph?.setDirtyCanvas(true)
}
@@ -100,7 +103,9 @@ export const useImageUploadWidget = () => {
// The value isnt set immediately so we need to wait a moment
// No change callbacks seem to be fired on initial setting of the value
requestAnimationFrame(() => {
nodeOutputStore.setNodeOutputs(node, fileComboWidget.value)
nodeOutputStore.setNodeOutputs(node, fileComboWidget.value, {
isAnimated
})
showPreview({ block: false })
})

View File

@@ -1,6 +1,17 @@
import { FirebaseOptions } from 'firebase/app'
export const FIREBASE_CONFIG: FirebaseOptions = {
// const DEV_CONFIG: FirebaseOptions = {
// apiKey: 'AIzaSyDa_YMeyzV0SkVe92vBZ1tVikWBmOU5KVE',
// authDomain: 'dreamboothy-dev.firebaseapp.com',
// databaseURL: 'https://dreamboothy-dev-default-rtdb.firebaseio.com',
// projectId: 'dreamboothy-dev',
// storageBucket: 'dreamboothy-dev.appspot.com',
// messagingSenderId: '313257147182',
// appId: '1:313257147182:web:be38f6ebf74345fc7618bf',
// measurementId: 'G-YEVSMYXSPY'
// }
const PROD_CONFIG: FirebaseOptions = {
apiKey: 'AIzaSyC2-fomLqgCjb7ELwta1I9cEarPK8ziTGs',
authDomain: 'dreamboothy.firebaseapp.com',
databaseURL: 'https://dreamboothy-default-rtdb.firebaseio.com',
@@ -10,3 +21,9 @@ export const FIREBASE_CONFIG: FirebaseOptions = {
appId: '1:357148958219:web:f5917f72e5f36a2015310e',
measurementId: 'G-3ZBD3MBTG4'
}
// To test with prod config while using dev server, set USE_PROD_FIREBASE_CONFIG=true in .env
// Otherwise, build with `npm run build` the and set `--front-end-root` to `ComfyUI_frontend/dist`
export const FIREBASE_CONFIG: FirebaseOptions = __USE_PROD_FIREBASE_CONFIG__
? PROD_CONFIG
: PROD_CONFIG // Just force prod to save time for now. change back later

View File

@@ -158,6 +158,27 @@ import { checkMirrorReachable } from '@/utils/networkUtil'
window.open('https://comfyorg.notion.site/', '_blank')
}
},
{
id: 'Comfy-Desktop.CheckForUpdates',
label: 'Check for Updates',
icon: 'pi pi-sync',
async function() {
const updateAvailable = await electronAPI.checkForUpdates({
disableUpdateReadyAction: true
})
if (updateAvailable.isUpdateAvailable) {
const version = updateAvailable.version
const proceed = await useDialogService().confirm({
title: t('desktopUpdate.updateFoundTitle', { version }),
message: t('desktopUpdate.updateAvailableMessage'),
type: 'default'
})
if (proceed) {
electronAPI.restartAndInstall()
}
}
}
},
{
id: 'Comfy-Desktop.Reinstall',
label: 'Reinstall',
@@ -223,7 +244,7 @@ import { checkMirrorReachable } from '@/utils/networkUtil'
},
{
path: ['Help'],
commands: ['Comfy-Desktop.Reinstall']
commands: ['Comfy-Desktop.CheckForUpdates', 'Comfy-Desktop.Reinstall']
}
],

View File

@@ -14,7 +14,6 @@ import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
import { useExtensionService } from '@/services/extensionService'
import { useLoad3dService } from '@/services/load3dService'
import { useToastStore } from '@/stores/toastStore'
import { generateUUID } from '@/utils/formatUtil'
useExtensionService().registerExtension({
name: 'Comfy.Load3D',
@@ -39,6 +38,16 @@ useExtensionService().registerExtension({
defaultValue: true,
experimental: true
},
{
id: 'Comfy.Load3D.BackgroundColor',
category: ['3D', 'Scene', 'Initial Background Color'],
name: 'Initial Background Color',
tooltip:
'Controls the default background color of the 3D scene. This setting determines the background appearance when a new 3D widget is created, but can be adjusted individually for each widget after creation.',
type: 'color',
defaultValue: '282828',
experimental: true
},
{
id: 'Comfy.Load3D.CameraType',
category: ['3D', 'Camera', 'Initial Camera Type'],
@@ -49,6 +58,51 @@ useExtensionService().registerExtension({
options: ['perspective', 'orthographic'],
defaultValue: 'perspective',
experimental: true
},
{
id: 'Comfy.Load3D.LightIntensity',
category: ['3D', 'Light', 'Initial Light Intensity'],
name: 'Initial Light Intensity',
tooltip:
'Sets the default brightness level of lighting in the 3D scene. This value determines how intensely lights illuminate objects when a new 3D widget is created, but can be adjusted individually for each widget after creation.',
type: 'number',
defaultValue: 3,
experimental: true
},
{
id: 'Comfy.Load3D.LightIntensityMaximum',
category: ['3D', 'Light', 'Light Intensity Maximum'],
name: 'Light Intensity Maximum',
tooltip:
'Sets the maximum allowable light intensity value for 3D scenes. This defines the upper brightness limit that can be set when adjusting lighting in any 3D widget.',
type: 'number',
defaultValue: 10,
experimental: true
},
{
id: 'Comfy.Load3D.LightIntensityMinimum',
category: ['3D', 'Light', 'Light Intensity Minimum'],
name: 'Light Intensity Minimum',
tooltip:
'Sets the minimum allowable light intensity value for 3D scenes. This defines the lower brightness limit that can be set when adjusting lighting in any 3D widget.',
type: 'number',
defaultValue: 1,
experimental: true
},
{
id: 'Comfy.Load3D.LightAdjustmentIncrement',
category: ['3D', 'Light', 'Light Adjustment Increment'],
name: 'Light Adjustment Increment',
tooltip:
'Controls the increment size when adjusting light intensity in 3D scenes. A smaller step value allows for finer control over lighting adjustments, while a larger value results in more noticeable changes per adjustment.',
type: 'slider',
attrs: {
min: 0.1,
max: 1,
step: 0.1
},
defaultValue: 0.5,
experimental: true
}
],
getCustomWidgets() {
@@ -118,7 +172,6 @@ useExtensionService().registerExtension({
}
const widget = new ComponentWidgetImpl({
id: generateUUID(),
node,
name: inputSpec.name,
component: Load3D,
@@ -259,7 +312,6 @@ useExtensionService().registerExtension({
}
const widget = new ComponentWidgetImpl({
id: generateUUID(),
node,
name: inputSpec.name,
component: Load3DAnimation,
@@ -355,7 +407,6 @@ useExtensionService().registerExtension({
}
const widget = new ComponentWidgetImpl({
id: generateUUID(),
node,
name: inputSpec.name,
component: Load3D,
@@ -432,7 +483,6 @@ useExtensionService().registerExtension({
}
const widget = new ComponentWidgetImpl({
id: generateUUID(),
node,
name: inputSpec.name,
component: Load3DAnimation,

View File

@@ -91,12 +91,18 @@ class Load3DConfiguration {
this.load3d.togglePreview(showPreview)
const bgColor = this.load3d.loadNodeProperty('Background Color', '#282828')
const bgColor = this.load3d.loadNodeProperty(
'Background Color',
'#' + useSettingStore().get('Comfy.Load3D.BackgroundColor')
)
this.load3d.setBackgroundColor(bgColor)
const lightIntensity: number = Number(
this.load3d.loadNodeProperty('Light Intensity', 5)
this.load3d.loadNodeProperty(
'Light Intensity',
useSettingStore().get('Comfy.Load3D.LightIntensity')
)
)
this.load3d.setLightIntensity(lightIntensity)

View File

@@ -2,16 +2,26 @@ import { applyTextReplacements } from '@/utils/searchAndReplace'
import { app } from '../../scripts/app'
const saveNodeTypes = new Set([
'SaveImage',
'SaveAnimatedWEBP',
'SaveWEBM',
'SaveAudio',
'SaveGLB',
'SaveAnimatedPNG',
'CLIPSave',
'VAESave',
'ModelSave',
'LoraSave',
'SaveLatent'
])
// Use widget values and dates in output filenames
app.registerExtension({
name: 'Comfy.SaveImageExtraOutput',
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (
nodeData.name === 'SaveImage' ||
nodeData.name === 'SaveAnimatedWEBP' ||
nodeData.name === 'SaveWEBM'
) {
if (saveNodeTypes.has(nodeData.name)) {
const onNodeCreated = nodeType.prototype.onNodeCreated
// When the SaveImage node is created we want to override the serialization of the output name widget to run our S&R
nodeType.prototype.onNodeCreated = function () {

View File

@@ -7,7 +7,6 @@ import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
import { useExtensionService } from '@/services/extensionService'
import { useLoad3dService } from '@/services/load3dService'
import { generateUUID } from '@/utils/formatUtil'
useExtensionService().registerExtension({
name: 'Comfy.SaveGLB',
@@ -30,7 +29,6 @@ useExtensionService().registerExtension({
}
const widget = new ComponentWidgetImpl({
id: generateUUID(),
node,
name: inputSpec.name,
component: Load3D,

View File

@@ -14,7 +14,8 @@ const isMediaUploadComboInput = (inputSpec: InputSpec) => {
const isUploadInput =
inputOptions['image_upload'] === true ||
inputOptions['video_upload'] === true
inputOptions['video_upload'] === true ||
inputOptions['animated_image_upload'] === true
return (
isUploadInput && (isComboInputSpecV1(inputSpec) || inputName === 'COMBO')

View File

@@ -18,16 +18,6 @@ import { mergeInputSpec } from '@/utils/nodeDefUtil'
import { applyTextReplacements } from '@/utils/searchAndReplace'
import { isPrimitiveNode } from '@/utils/typeGuardUtil'
const VALID_TYPES = [
'STRING',
'combo',
'number',
'toggle',
'BOOLEAN',
'text',
'string'
]
const replacePropertyName = 'Run widget replace on values'
export class PrimitiveNode extends LGraphNode {
controlValues?: any[]
@@ -47,7 +37,7 @@ export class PrimitiveNode extends LGraphNode {
applyToGraph(extraLinks: LLink[] = []) {
if (!this.outputs[0].links?.length) return
let links = [
const links = [
...this.outputs[0].links.map((l) => app.graph.links[l]),
...extraLinks
]
@@ -58,30 +48,35 @@ export class PrimitiveNode extends LGraphNode {
// For each output link copy our value over the original widget value
for (const linkInfo of links) {
// @ts-expect-error fixme ts strict error
const node = this.graph.getNodeById(linkInfo.target_id)
// @ts-expect-error fixme ts strict error
const input = node.inputs[linkInfo.target_slot]
let widget: IWidget | undefined
const widgetName = (input.widget as { name: string }).name
if (widgetName) {
// @ts-expect-error fixme ts strict error
widget = node.widgets.find((w) => w.name === widgetName)
const node = this.graph?.getNodeById(linkInfo.target_id)
const input = node?.inputs[linkInfo.target_slot]
if (!input) {
console.warn('Unable to resolve node or input for link', linkInfo)
continue
}
if (widget) {
widget.value = v
if (widget.callback) {
widget.callback(
widget.value,
app.canvas,
// @ts-expect-error fixme ts strict error
node,
app.canvas.graph_mouse,
{} as CanvasMouseEvent
)
}
const widgetName = input.widget?.name
if (!widgetName) {
console.warn('Invalid widget or widget name', input.widget)
continue
}
const widget = node.widgets?.find((w) => w.name === widgetName)
if (!widget) {
console.warn(
`Unable to find widget "${widgetName}" on node [${node.id}]`
)
continue
}
widget.value = v
widget.callback?.(
widget.value,
app.canvas,
node,
app.canvas.graph_mouse,
{} as CanvasMouseEvent
)
}
}
@@ -347,31 +342,6 @@ export class PrimitiveNode extends LGraphNode {
}
}
isValidWidgetLink(
originSlot: number,
targetNode: LGraphNode,
targetWidget: IWidget
) {
const config2 = getConfig.call(targetNode, targetWidget.name) ?? [
targetWidget.type,
targetWidget.options || {}
]
if (!isConvertibleWidget(targetWidget, config2)) return false
const output = this.outputs[originSlot]
if (
!(
output.widget?.[CONFIG] ??
(output.widget?.[GET_CONFIG] as () => InputSpec)?.()
)
) {
// No widget defined for this primitive yet so allow it
return true
}
return !!mergeIfValid.call(this, output, config2)
}
#isValidConnection(input: INodeInputSlot, forceUpdate?: boolean) {
// Only allow connections where the configs match
const output = this.outputs?.[0]
@@ -440,13 +410,6 @@ function getConfig(this: LGraphNode, widgetName: string) {
)
}
function isConvertibleWidget(widget: IWidget, config: InputSpec): boolean {
return (
// @ts-expect-error InputSpec is not typed correctly
VALID_TYPES.includes(widget.type) || VALID_TYPES.includes(config[0])
)
}
/**
* Convert a widget to an input slot.
* @deprecated Widget to socket conversion is no longer necessary, as they co-exist now.

View File

@@ -1,4 +1,7 @@
{
"Comfy-Desktop_CheckForUpdates": {
"label": "Check for Updates"
},
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
"label": "Open Custom Nodes Folder"
},
@@ -170,6 +173,9 @@
"Comfy_Undo": {
"label": "Undo"
},
"Comfy_User_OpenSignInDialog": {
"label": "Open Sign In Dialog"
},
"Workspace_CloseWorkflow": {
"label": "Close Current Workflow"
},

View File

@@ -110,7 +110,8 @@
"migrate": "Migrate",
"updateAvailable": "Update Available",
"login": "Login",
"learnMore": "Learn more"
"learnMore": "Learn more",
"amount": "Amount"
},
"manager": {
"title": "Custom Nodes Manager",
@@ -463,8 +464,7 @@
"ControlNet": "ControlNet",
"Upscaling": "Upscaling",
"Video": "Video",
"SD3_5": "SD3.5",
"SDXL": "SDXL",
"Image": "Image",
"Area Composition": "Area Composition",
"3D": "3D",
"Audio": "Audio"
@@ -510,20 +510,23 @@
"ltxv_image_to_video": "LTXV Image to Video",
"ltxv_text_to_video": "LTXV Text to Video",
"mochi_text_to_video_example": "Mochi Text to Video",
"hunyuan_video_text_to_video": "Hunyuan Video Text to Video"
"hunyuan_video_text_to_video": "Hunyuan Video Text to Video",
"wan2_1_fun_inp": "Wan 2.1 Inpainting",
"wan2_1_fun_control": "Wan 2.1 ControlNet"
},
"SD3_5": {
"Image": {
"sd3_5_simple_example": "SD3.5 Simple",
"sd3_5_large_canny_controlnet_example": "SD3.5 Large Canny ControlNet",
"sd3_5_large_depth": "SD3.5 Large Depth",
"sd3_5_large_blur": "SD3.5 Large Blur"
},
"SDXL": {
"sd3_5_large_blur": "SD3.5 Large Blur",
"sdxl_simple_example": "SDXL Simple",
"sdxl_refiner_prompt_example": "SDXL Refiner Prompt",
"sdxl_revision_text_prompts": "SDXL Revision Text Prompts",
"sdxl_revision_zero_positive": "SDXL Revision Zero Positive",
"sdxlturbo_example": "SDXL Turbo"
"sdxlturbo_example": "SDXL Turbo",
"hidream_i1_dev": "HiDream I1 Dev",
"hidream_i1_fast": "HiDream I1 Fast",
"hidream_i1_full": "HiDream I1 Full"
},
"Area Composition": {
"area_composition": "Area Composition",
@@ -600,6 +603,7 @@
"Workflow": "Workflow",
"Edit": "Edit",
"Help": "Help",
"Check for Updates": "Check for Updates",
"Open Custom Nodes Folder": "Open Custom Nodes Folder",
"Open Inputs Folder": "Open Inputs Folder",
"Open Logs Folder": "Open Logs Folder",
@@ -657,6 +661,7 @@
"Show Settings Dialog": "Show Settings Dialog",
"Toggle Theme (Dark/Light)": "Toggle Theme (Dark/Light)",
"Undo": "Undo",
"Open Sign In Dialog": "Open Sign In Dialog",
"Close Current Workflow": "Close Current Workflow",
"Next Opened Workflow": "Next Opened Workflow",
"Previous Opened Workflow": "Previous Opened Workflow",
@@ -722,7 +727,8 @@
"Load 3D": "Load 3D",
"Camera": "Camera",
"Scene": "Scene",
"3D": "3D"
"3D": "3D",
"Light": "Light"
},
"serverConfigItems": {
"listen": {
@@ -990,7 +996,9 @@
"desktopUpdate": {
"title": "Updating ComfyUI Desktop",
"description": "ComfyUI Desktop is installing new dependencies. This may take a few minutes.",
"terminalDefaultMessage": "Any console output from the update will be shown here."
"terminalDefaultMessage": "Any console output from the update will be shown here.",
"updateFoundTitle": "Update Found (v{version})",
"updateAvailableMessage": "An update is available. Do you want to restart and update now?"
},
"clipboard": {
"successMessage": "Copied to clipboard",
@@ -1055,6 +1063,7 @@
"auth": {
"login": {
"title": "Log in to your account",
"signInOrSignUp": "Sign In / Sign Up",
"newUser": "New here?",
"signUp": "Sign up",
"emailLabel": "Email",
@@ -1073,7 +1082,8 @@
"andText": "and",
"privacyLink": "Privacy Policy",
"success": "Login successful",
"failed": "Login failed"
"failed": "Login failed",
"genericErrorMessage": "Sorry, we've encountered an error. Please contact {supportEmail}."
},
"signup": {
"title": "Create an account",
@@ -1086,6 +1096,25 @@
"signIn": "Sign in",
"signUpWithGoogle": "Sign up with Google",
"signUpWithGithub": "Sign up with Github"
},
"signOut": {
"signOut": "Log Out",
"success": "Signed out successfully",
"successDetail": "You have been signed out of your account."
},
"required": {
"signIn": {
"title": "Sign-In Required to Execute Workflow",
"message": "This workflow includes nodes that require an active account. Please log in or create one to continue.",
"hint": "To login go to: Settings > User > Login",
"action": "Open Settings to Login"
},
"credits": {
"title": "Credits Required to Execute Workflow",
"message": "This workflow includes nodes that require credits. Please add credits to your account to continue.",
"hint": "To add credits go to: Settings > User > Credits",
"action": "Open Settings to Add Credits"
}
}
},
"validation": {
@@ -1102,5 +1131,33 @@
"special": "Must contain at least one special character",
"match": "Passwords must match"
}
},
"credits": {
"credits": "Credits",
"yourCreditBalance": "Your credit balance",
"purchaseCredits": "Purchase Credits",
"creditsHistory": "Credits History",
"faqs": "FAQs",
"messageSupport": "Message Support",
"lastUpdated": "Last updated",
"topUp": {
"title": "Add to Credit Balance",
"insufficientTitle": "Insufficient Credits",
"insufficientMessage": "You don't have enough credits to run this workflow.",
"addCredits": "Add credits to your balance",
"maxAmount": "(Max. $1,000 USD)",
"buyNow": "Buy now"
}
},
"userSettings": {
"title": "User Settings",
"name": "Name",
"email": "Email",
"notSet": "Not set",
"provider": "Sign in method",
"providers": {
"google": "Google",
"github": "GitHub"
}
}
}

View File

@@ -108,6 +108,10 @@
"Hidden": "Hidden"
}
},
"Comfy_Load3D_BackgroundColor": {
"name": "Initial Background Color",
"tooltip": "Controls the default background color of the 3D scene. This setting determines the background appearance when a new 3D widget is created, but can be adjusted individually for each widget after creation."
},
"Comfy_Load3D_CameraType": {
"name": "Initial Camera Type",
"tooltip": "Controls whether the camera is perspective or orthographic by default when a new 3D widget is created. This default can still be toggled individually for each widget after creation.",
@@ -116,6 +120,22 @@
"orthographic": "orthographic"
}
},
"Comfy_Load3D_LightAdjustmentIncrement": {
"name": "Light Adjustment Increment",
"tooltip": "Controls the increment size when adjusting light intensity in 3D scenes. A smaller step value allows for finer control over lighting adjustments, while a larger value results in more noticeable changes per adjustment."
},
"Comfy_Load3D_LightIntensity": {
"name": "Initial Light Intensity",
"tooltip": "Sets the default brightness level of lighting in the 3D scene. This value determines how intensely lights illuminate objects when a new 3D widget is created, but can be adjusted individually for each widget after creation."
},
"Comfy_Load3D_LightIntensityMaximum": {
"name": "Light Intensity Maximum",
"tooltip": "Sets the maximum allowable light intensity value for 3D scenes. This defines the upper brightness limit that can be set when adjusting lighting in any 3D widget."
},
"Comfy_Load3D_LightIntensityMinimum": {
"name": "Light Intensity Minimum",
"tooltip": "Sets the minimum allowable light intensity value for 3D scenes. This defines the lower brightness limit that can be set when adjusting lighting in any 3D widget."
},
"Comfy_Load3D_ShowGrid": {
"name": "Initial Grid Visibility",
"tooltip": "Controls whether the grid is visible by default when a new 3D widget is created. This default can still be toggled individually for each widget after creation."

View File

@@ -1,4 +1,7 @@
{
"Comfy-Desktop_CheckForUpdates": {
"label": "Buscar actualizaciones"
},
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
"label": "Abrir carpeta de nodos personalizados"
},
@@ -170,6 +173,9 @@
"Comfy_Undo": {
"label": "Deshacer"
},
"Comfy_User_OpenSignInDialog": {
"label": "Abrir diálogo de inicio de sesión"
},
"Workspace_CloseWorkflow": {
"label": "Cerrar Flujo de Trabajo Actual"
},

View File

@@ -17,6 +17,7 @@
"emailPlaceholder": "Ingresa tu correo electrónico",
"failed": "Inicio de sesión fallido",
"forgotPassword": "¿Olvidaste tu contraseña?",
"genericErrorMessage": "Lo sentimos, hemos encontrado un error. Por favor, contacta con {supportEmail}.",
"loginButton": "Iniciar sesión",
"loginWithGithub": "Iniciar sesión con Github",
"loginWithGoogle": "Iniciar sesión con Google",
@@ -25,12 +26,32 @@
"passwordLabel": "Contraseña",
"passwordPlaceholder": "Ingresa tu contraseña",
"privacyLink": "Política de privacidad",
"signInOrSignUp": "Iniciar sesión / Registrarse",
"signUp": "Regístrate",
"success": "Inicio de sesión exitoso",
"termsLink": "Términos de uso",
"termsText": "Al hacer clic en \"Siguiente\" o \"Registrarse\", aceptas nuestros",
"title": "Inicia sesión en tu cuenta"
},
"required": {
"credits": {
"action": "Abrir configuración para añadir créditos",
"hint": "Para añadir créditos ve a: Configuración > Usuario > Créditos",
"message": "Este flujo de trabajo incluye nodos que requieren créditos. Por favor, añade créditos a tu cuenta para continuar.",
"title": "Créditos requeridos para ejecutar el flujo de trabajo"
},
"signIn": {
"action": "Abrir configuración para iniciar sesión",
"hint": "Para iniciar sesión ve a: Configuración > Usuario > Iniciar sesión",
"message": "Este flujo de trabajo incluye nodos que requieren una cuenta activa. Por favor, inicia sesión o crea una para continuar.",
"title": "Inicio de sesión requerido para ejecutar el flujo de trabajo"
}
},
"signOut": {
"signOut": "Cerrar sesión",
"success": "Sesión cerrada correctamente",
"successDetail": "Has cerrado sesión en tu cuenta."
},
"signup": {
"alreadyHaveAccount": "¿Ya tienes una cuenta?",
"emailLabel": "Correo electrónico",
@@ -92,6 +113,23 @@
"Title": "Título",
"Unpin": "Desanclar"
},
"credits": {
"credits": "Créditos",
"creditsHistory": "Historial de créditos",
"faqs": "Preguntas frecuentes",
"lastUpdated": "Última actualización",
"messageSupport": "Contactar soporte",
"purchaseCredits": "Comprar créditos",
"topUp": {
"addCredits": "Agregar créditos a tu saldo",
"buyNow": "Comprar ahora",
"insufficientMessage": "No tienes suficientes créditos para ejecutar este flujo de trabajo.",
"insufficientTitle": "Créditos insuficientes",
"maxAmount": "(Máx. $1,000 USD)",
"title": "Agregar al saldo de créditos"
},
"yourCreditBalance": "Tu saldo de créditos"
},
"dataTypes": {
"AUDIO": "AUDIO",
"BOOLEAN": "BOOLEANO",
@@ -137,7 +175,9 @@
"desktopUpdate": {
"description": "ComfyUI Desktop está instalando nuevas dependencias. Esto puede tardar unos minutos.",
"terminalDefaultMessage": "Cualquier salida de consola de la actualización se mostrará aquí.",
"title": "Actualizando ComfyUI Desktop"
"title": "Actualizando ComfyUI Desktop",
"updateAvailableMessage": "Hay una actualización disponible. ¿Quieres reiniciar y actualizar ahora?",
"updateFoundTitle": "Actualización encontrada (v{version})"
},
"downloadGit": {
"gitWebsite": "Descargar git",
@@ -166,6 +206,7 @@
"about": "Acerca de",
"add": "Añadir",
"all": "Todo",
"amount": "Cantidad",
"apply": "Aplicar",
"back": "Atrás",
"cancel": "Cancelar",
@@ -565,6 +606,7 @@
"Bypass/Unbypass Selected Nodes": "Evitar/No evitar nodos seleccionados",
"Canvas Toggle Link Visibility": "Alternar visibilidad de enlace en lienzo",
"Canvas Toggle Lock": "Alternar bloqueo en lienzo",
"Check for Updates": "Buscar actualizaciones",
"Clear Pending Tasks": "Borrar tareas pendientes",
"Clear Workflow": "Borrar flujo de trabajo",
"Clipspace": "Espacio de clip",
@@ -600,6 +642,7 @@
"Open Logs Folder": "Abrir carpeta de registros",
"Open Models Folder": "Abrir carpeta de modelos",
"Open Outputs Folder": "Abrir carpeta de salidas",
"Open Sign In Dialog": "Abrir diálogo de inicio de sesión",
"Open extra_model_paths_yaml": "Abrir extra_model_paths.yaml",
"Pin/Unpin Selected Items": "Anclar/Desanclar elementos seleccionados",
"Pin/Unpin Selected Nodes": "Anclar/Desanclar nodos seleccionados",
@@ -877,6 +920,7 @@
"Graph": "Gráfico",
"Group": "Grupo",
"Keybinding": "Asignación de Teclas",
"Light": "Claro",
"Link": "Enlace",
"LinkRelease": "Liberación de Enlace",
"LiteGraph": "Lite Graph",
@@ -969,8 +1013,7 @@
"ControlNet": "ControlNet",
"Custom Nodes": "Nodos Personalizados",
"Flux": "Flux",
"SD3_5": "SD3.5",
"SDXL": "SDXL",
"Image": "Imagen",
"Upscaling": "Ampliación",
"Video": "Video"
},
@@ -1015,13 +1058,14 @@
"flux_redux_model_example": "Flux Redux Model",
"flux_schnell": "Flux Schnell"
},
"SD3_5": {
"Image": {
"hidream_i1_dev": "HiDream I1 Dev",
"hidream_i1_fast": "HiDream I1 Rápido",
"hidream_i1_full": "HiDream I1 Completo",
"sd3_5_large_blur": "SD3.5 Grande Desenfoque",
"sd3_5_large_canny_controlnet_example": "SD3.5 Grande Canny ControlNet",
"sd3_5_large_depth": "SD3.5 Grande Profundidad",
"sd3_5_simple_example": "SD3.5 Simple"
},
"SDXL": {
"sd3_5_simple_example": "SD3.5 Simple",
"sdxl_refiner_prompt_example": "SDXL Refinador de Solicitud",
"sdxl_revision_text_prompts": "SDXL Revisión de Solicitud de Texto",
"sdxl_revision_zero_positive": "SDXL Revisión Cero Positivo",
@@ -1042,7 +1086,9 @@
"ltxv_text_to_video": "LTXV Texto a Video",
"mochi_text_to_video_example": "Mochi Texto a Video",
"text_to_video_wan": "Wan 2.1 Texto a Video",
"txt_to_image_to_video": "SVD Texto a Imagen a Video"
"txt_to_image_to_video": "SVD Texto a Imagen a Video",
"wan2_1_fun_control": "Wan 2.1 ControlNet",
"wan2_1_fun_inp": "Wan 2.1 Relleno"
}
},
"title": "Comienza con una Plantilla"
@@ -1079,6 +1125,17 @@
"next": "Siguiente",
"selectUser": "Selecciona un usuario"
},
"userSettings": {
"email": "Correo electrónico",
"name": "Nombre",
"notSet": "No establecido",
"provider": "Método de inicio de sesión",
"providers": {
"github": "GitHub",
"google": "Google"
},
"title": "Configuración de usuario"
},
"validation": {
"invalidEmail": "Dirección de correo electrónico inválida",
"maxLength": "No debe tener más de {length} caracteres",

View File

@@ -108,6 +108,10 @@
"Straight": "Recto"
}
},
"Comfy_Load3D_BackgroundColor": {
"name": "Color de fondo inicial",
"tooltip": "Controla el color de fondo predeterminado de la escena 3D. Esta configuración determina la apariencia del fondo cuando se crea un nuevo widget 3D, pero puede ajustarse individualmente para cada widget después de su creación."
},
"Comfy_Load3D_CameraType": {
"name": "Tipo de Cámara",
"options": {
@@ -116,6 +120,22 @@
},
"tooltip": "Controla si la cámara es perspectiva u ortográfica por defecto cuando se crea un nuevo widget 3D. Este valor predeterminado aún puede ser alternado individualmente para cada widget después de su creación."
},
"Comfy_Load3D_LightAdjustmentIncrement": {
"name": "Incremento de ajuste de luz",
"tooltip": "Controla el tamaño del incremento al ajustar la intensidad de la luz en escenas 3D. Un valor de paso más pequeño permite un control más preciso sobre los ajustes de iluminación, mientras que un valor más grande resulta en cambios más notorios por cada ajuste."
},
"Comfy_Load3D_LightIntensity": {
"name": "Intensidad Inicial de la Luz",
"tooltip": "Establece el nivel de brillo predeterminado de la iluminación en la escena 3D. Este valor determina cuán intensamente las luces iluminan los objetos cuando se crea un nuevo widget 3D, pero puede ajustarse individualmente para cada widget después de la creación."
},
"Comfy_Load3D_LightIntensityMaximum": {
"name": "Intensidad Máxima de Luz",
"tooltip": "Establece el valor máximo permitido de intensidad de luz para escenas 3D. Esto define el límite superior de brillo que se puede ajustar al modificar la iluminación en cualquier widget 3D."
},
"Comfy_Load3D_LightIntensityMinimum": {
"name": "Intensidad de luz mínima",
"tooltip": "Establece el valor mínimo permitido de intensidad de luz para escenas 3D. Esto define el límite inferior de brillo que se puede ajustar al modificar la iluminación en cualquier widget 3D."
},
"Comfy_Load3D_ShowGrid": {
"name": "Mostrar Cuadrícula",
"tooltip": "Cambiar para mostrar cuadrícula por defecto"

View File

@@ -1,4 +1,7 @@
{
"Comfy-Desktop_CheckForUpdates": {
"label": "Vérifier les mises à jour"
},
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
"label": "Ouvrir le dossier des nœuds personnalisés"
},
@@ -170,6 +173,9 @@
"Comfy_Undo": {
"label": "Annuler"
},
"Comfy_User_OpenSignInDialog": {
"label": "Ouvrir la boîte de dialogue de connexion"
},
"Workspace_CloseWorkflow": {
"label": "Fermer le flux de travail actuel"
},

View File

@@ -17,6 +17,7 @@
"emailPlaceholder": "Entrez votre email",
"failed": "Échec de la connexion",
"forgotPassword": "Mot de passe oublié?",
"genericErrorMessage": "Désolé, une erreur s'est produite. Veuillez contacter {supportEmail}.",
"loginButton": "Se connecter",
"loginWithGithub": "Se connecter avec Github",
"loginWithGoogle": "Se connecter avec Google",
@@ -25,12 +26,32 @@
"passwordLabel": "Mot de passe",
"passwordPlaceholder": "Entrez votre mot de passe",
"privacyLink": "Politique de confidentialité",
"signInOrSignUp": "Se connecter / Sinscrire",
"signUp": "S'inscrire",
"success": "Connexion réussie",
"termsLink": "Conditions d'utilisation",
"termsText": "En cliquant sur \"Suivant\" ou \"S'inscrire\", vous acceptez nos",
"title": "Connectez-vous à votre compte"
},
"required": {
"credits": {
"action": "Ouvrir les paramètres pour ajouter des crédits",
"hint": "Pour ajouter des crédits, allez dans : Paramètres > Utilisateur > Crédits",
"message": "Ce workflow inclut des nœuds nécessitant des crédits. Veuillez ajouter des crédits à votre compte pour continuer.",
"title": "Crédits requis pour exécuter le workflow"
},
"signIn": {
"action": "Ouvrir les paramètres pour se connecter",
"hint": "Pour vous connecter, allez dans : Paramètres > Utilisateur > Connexion",
"message": "Ce workflow inclut des nœuds nécessitant un compte actif. Veuillez vous connecter ou en créer un pour continuer.",
"title": "Connexion requise pour exécuter le workflow"
}
},
"signOut": {
"signOut": "Se déconnecter",
"success": "Déconnexion réussie",
"successDetail": "Vous avez été déconnecté de votre compte."
},
"signup": {
"alreadyHaveAccount": "Vous avez déjà un compte?",
"emailLabel": "Email",
@@ -92,6 +113,23 @@
"Title": "Titre",
"Unpin": "Désépingler"
},
"credits": {
"credits": "Crédits",
"creditsHistory": "Historique des crédits",
"faqs": "FAQ",
"lastUpdated": "Dernière mise à jour",
"messageSupport": "Contacter le support",
"purchaseCredits": "Acheter des crédits",
"topUp": {
"addCredits": "Ajouter des crédits à votre solde",
"buyNow": "Acheter maintenant",
"insufficientMessage": "Vous n'avez pas assez de crédits pour exécuter ce workflow.",
"insufficientTitle": "Crédits insuffisants",
"maxAmount": "(Max. 1 000 $ US)",
"title": "Ajouter au solde de crédits"
},
"yourCreditBalance": "Votre solde de crédits"
},
"dataTypes": {
"AUDIO": "AUDIO",
"BOOLEAN": "BOOLEAN",
@@ -137,7 +175,9 @@
"desktopUpdate": {
"description": "ComfyUI Desktop installe de nouvelles dépendances. Cela peut prendre quelques minutes.",
"terminalDefaultMessage": "Toute sortie de console de la mise à jour sera affichée ici.",
"title": "Mise à jour de ComfyUI Desktop"
"title": "Mise à jour de ComfyUI Desktop",
"updateAvailableMessage": "Une mise à jour est disponible. Voulez-vous redémarrer et mettre à jour maintenant?",
"updateFoundTitle": "Mise à jour trouvée (v{version})"
},
"downloadGit": {
"gitWebsite": "Télécharger git",
@@ -166,6 +206,7 @@
"about": "À propos",
"add": "Ajouter",
"all": "Tout",
"amount": "Quantité",
"apply": "Appliquer",
"back": "Retour",
"cancel": "Annuler",
@@ -565,6 +606,7 @@
"Bypass/Unbypass Selected Nodes": "Contourner/Ne pas contourner les nœuds sélectionnés",
"Canvas Toggle Link Visibility": "Basculer la visibilité du lien de la toile",
"Canvas Toggle Lock": "Basculer le verrouillage de la toile",
"Check for Updates": "Vérifier les mises à jour",
"Clear Pending Tasks": "Effacer les tâches en attente",
"Clear Workflow": "Effacer le flux de travail",
"Clipspace": "Espace de clip",
@@ -600,6 +642,7 @@
"Open Logs Folder": "Ouvrir le dossier des journaux",
"Open Models Folder": "Ouvrir le dossier des modèles",
"Open Outputs Folder": "Ouvrir le dossier des sorties",
"Open Sign In Dialog": "Ouvrir la boîte de dialogue de connexion",
"Open extra_model_paths_yaml": "Ouvrir extra_model_paths.yaml",
"Pin/Unpin Selected Items": "Épingler/Désépingler les éléments sélectionnés",
"Pin/Unpin Selected Nodes": "Épingler/Désépingler les nœuds sélectionnés",
@@ -877,6 +920,7 @@
"Graph": "Graphique",
"Group": "Groupe",
"Keybinding": "Raccourci Clavier",
"Light": "Clair",
"Link": "Lien",
"LinkRelease": "Libération de Lien",
"LiteGraph": "Lite Graph",
@@ -969,8 +1013,7 @@
"ControlNet": "ControlNet",
"Custom Nodes": "Nœuds personnalisés",
"Flux": "Flux",
"SD3_5": "SD3.5",
"SDXL": "SDXL",
"Image": "Image",
"Upscaling": "Mise à l'échelle",
"Video": "Vidéo"
},
@@ -1015,13 +1058,14 @@
"flux_redux_model_example": "Flux Redux Model",
"flux_schnell": "Flux Schnell"
},
"SD3_5": {
"Image": {
"hidream_i1_dev": "HiDream I1 Dev",
"hidream_i1_fast": "HiDream I1 Rapide",
"hidream_i1_full": "HiDream I1 Complet",
"sd3_5_large_blur": "SD3.5 Grand Flou",
"sd3_5_large_canny_controlnet_example": "SD3.5 Grand Canny ControlNet",
"sd3_5_large_depth": "SD3.5 Grande Profondeur",
"sd3_5_simple_example": "SD3.5 Simple"
},
"SDXL": {
"sd3_5_simple_example": "SD3.5 Simple",
"sdxl_refiner_prompt_example": "SDXL Refiner Prompt",
"sdxl_revision_text_prompts": "Révisions de Texte SDXL",
"sdxl_revision_zero_positive": "Révision Zéro Positive SDXL",
@@ -1042,7 +1086,9 @@
"ltxv_text_to_video": "LTXV Texte à Vidéo",
"mochi_text_to_video_example": "Exemple de Texte à Vidéo Mochi",
"text_to_video_wan": "Wan 2.1 Texte à Vidéo",
"txt_to_image_to_video": "Texte à Image à Vidéo"
"txt_to_image_to_video": "Texte à Image à Vidéo",
"wan2_1_fun_control": "Wan 2.1 ControlNet",
"wan2_1_fun_inp": "Wan 2.1 Inpainting"
}
},
"title": "Commencez avec un modèle"
@@ -1079,6 +1125,17 @@
"next": "Suivant",
"selectUser": "Sélectionnez un utilisateur"
},
"userSettings": {
"email": "E-mail",
"name": "Nom",
"notSet": "Non défini",
"provider": "Méthode de connexion",
"providers": {
"github": "GitHub",
"google": "Google"
},
"title": "Paramètres utilisateur"
},
"validation": {
"invalidEmail": "Adresse e-mail invalide",
"maxLength": "Ne doit pas dépasser {length} caractères",

View File

@@ -108,6 +108,10 @@
"Straight": "Droit"
}
},
"Comfy_Load3D_BackgroundColor": {
"name": "Couleur de fond initiale",
"tooltip": "Contrôle la couleur de fond par défaut de la scène 3D. Ce paramètre détermine l'apparence du fond lors de la création d'un nouveau widget 3D, mais peut être ajusté individuellement pour chaque widget après la création."
},
"Comfy_Load3D_CameraType": {
"name": "Type de Caméra",
"options": {
@@ -116,6 +120,22 @@
},
"tooltip": "Contrôle si la caméra est en perspective ou orthographique par défaut lorsqu'un nouveau widget 3D est créé. Ce défaut peut toujours être basculé individuellement pour chaque widget après sa création."
},
"Comfy_Load3D_LightAdjustmentIncrement": {
"name": "Incrément d'ajustement de la lumière",
"tooltip": "Contrôle la taille de l'incrément lors de l'ajustement de l'intensité lumineuse dans les scènes 3D. Une valeur de pas plus petite permet un contrôle plus précis des ajustements de lumière, tandis qu'une valeur plus grande entraîne des changements plus visibles à chaque ajustement."
},
"Comfy_Load3D_LightIntensity": {
"name": "Intensité lumineuse initiale",
"tooltip": "Définit le niveau de luminosité par défaut de léclairage dans la scène 3D. Cette valeur détermine lintensité avec laquelle les lumières illuminent les objets lors de la création dun nouveau widget 3D, mais peut être ajustée individuellement pour chaque widget après la création."
},
"Comfy_Load3D_LightIntensityMaximum": {
"name": "Intensité lumineuse maximale",
"tooltip": "Définit la valeur maximale autorisée pour lintensité lumineuse dans les scènes 3D. Cela fixe la limite supérieure de luminosité pouvant être réglée lors de lajustement de léclairage dans tout widget 3D."
},
"Comfy_Load3D_LightIntensityMinimum": {
"name": "Intensité lumineuse minimale",
"tooltip": "Définit la valeur minimale autorisée de lintensité lumineuse pour les scènes 3D. Cela définit la limite inférieure de luminosité pouvant être réglée lors de lajustement de léclairage dans tout widget 3D."
},
"Comfy_Load3D_ShowGrid": {
"name": "Afficher la Grille",
"tooltip": "Basculer pour afficher la grille par défaut"

View File

@@ -1,4 +1,7 @@
{
"Comfy-Desktop_CheckForUpdates": {
"label": "更新を確認する"
},
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
"label": "カスタムノードフォルダを開く"
},
@@ -170,6 +173,9 @@
"Comfy_Undo": {
"label": "元に戻す"
},
"Comfy_User_OpenSignInDialog": {
"label": "サインインダイアログを開く"
},
"Workspace_CloseWorkflow": {
"label": "現在のワークフローを閉じる"
},

View File

@@ -17,6 +17,7 @@
"emailPlaceholder": "メールアドレスを入力してください",
"failed": "ログイン失敗",
"forgotPassword": "パスワードを忘れましたか?",
"genericErrorMessage": "申し訳ありませんが、エラーが発生しました。{supportEmail} までご連絡ください。",
"loginButton": "ログイン",
"loginWithGithub": "Githubでログイン",
"loginWithGoogle": "Googleでログイン",
@@ -25,12 +26,32 @@
"passwordLabel": "パスワード",
"passwordPlaceholder": "パスワードを入力してください",
"privacyLink": "プライバシーポリシー",
"signInOrSignUp": "サインイン / サインアップ",
"signUp": "サインアップ",
"success": "ログイン成功",
"termsLink": "利用規約",
"termsText": "「次へ」または「サインアップ」をクリックすると、私たちの",
"title": "アカウントにログインする"
},
"required": {
"credits": {
"action": "設定を開いてクレジットを追加",
"hint": "クレジットを追加するには: 設定 > ユーザー > クレジット",
"message": "このワークフローにはクレジットが必要なノードが含まれています。続行するにはアカウントにクレジットを追加してください。",
"title": "ワークフロー実行にはクレジットが必要です"
},
"signIn": {
"action": "設定を開いてログイン",
"hint": "ログインするには: 設定 > ユーザー > ログイン",
"message": "このワークフローにはアクティブなアカウントが必要なノードが含まれています。続行するにはログインまたはアカウントを作成してください。",
"title": "ワークフロー実行にはサインインが必要です"
}
},
"signOut": {
"signOut": "ログアウト",
"success": "正常にサインアウトしました",
"successDetail": "アカウントからサインアウトしました。"
},
"signup": {
"alreadyHaveAccount": "すでにアカウントをお持ちですか?",
"emailLabel": "メール",
@@ -92,6 +113,23 @@
"Title": "タイトル",
"Unpin": "ピンを解除"
},
"credits": {
"credits": "クレジット",
"creditsHistory": "クレジット履歴",
"faqs": "よくある質問",
"lastUpdated": "最終更新",
"messageSupport": "サポートにメッセージ",
"purchaseCredits": "クレジットを購入",
"topUp": {
"addCredits": "残高にクレジットを追加",
"buyNow": "今すぐ購入",
"insufficientMessage": "このワークフローを実行するのに十分なクレジットがありません。",
"insufficientTitle": "クレジット不足",
"maxAmount": "(最大 $1,000 USD",
"title": "クレジット残高を追加"
},
"yourCreditBalance": "あなたのクレジット残高"
},
"dataTypes": {
"AUDIO": "オーディオ",
"BOOLEAN": "ブール",
@@ -137,7 +175,9 @@
"desktopUpdate": {
"description": "ComfyUIデスクトップは新しい依存関係をインストールしています。これには数分かかる場合があります。",
"terminalDefaultMessage": "更新からの任意のコンソール出力はここに表示されます。",
"title": "ComfyUIデスクトップの更新"
"title": "ComfyUIデスクトップの更新",
"updateAvailableMessage": "アップデートが利用可能です。今すぐ再起動してアップデートしますか?",
"updateFoundTitle": "アップデートが見つかりました (v{version})"
},
"downloadGit": {
"gitWebsite": "Gitをダウンロード",
@@ -166,6 +206,7 @@
"about": "情報",
"add": "追加",
"all": "すべて",
"amount": "量",
"apply": "適用する",
"back": "戻る",
"cancel": "キャンセル",
@@ -565,6 +606,7 @@
"Bypass/Unbypass Selected Nodes": "選択したノードのバイパス/バイパス解除",
"Canvas Toggle Link Visibility": "キャンバスのリンク表示を切り替え",
"Canvas Toggle Lock": "キャンバスのロックを切り替え",
"Check for Updates": "更新を確認する",
"Clear Pending Tasks": "保留中のタスクをクリア",
"Clear Workflow": "ワークフローをクリア",
"Clipspace": "クリップスペース",
@@ -600,6 +642,7 @@
"Open Logs Folder": "ログフォルダを開く",
"Open Models Folder": "モデルフォルダを開く",
"Open Outputs Folder": "出力フォルダを開く",
"Open Sign In Dialog": "サインインダイアログを開く",
"Open extra_model_paths_yaml": "extra_model_paths.yamlを開く",
"Pin/Unpin Selected Items": "選択したアイテムのピン留め/ピン留め解除",
"Pin/Unpin Selected Nodes": "選択したノードのピン留め/ピン留め解除",
@@ -877,6 +920,7 @@
"Graph": "グラフ",
"Group": "グループ",
"Keybinding": "キー割り当て",
"Light": "ライト",
"Link": "リンク",
"LinkRelease": "リンク解除",
"LiteGraph": "Lite Graph",
@@ -969,8 +1013,7 @@
"ControlNet": "ControlNet",
"Custom Nodes": "カスタムノード",
"Flux": "Flux",
"SD3_5": "SD3.5",
"SDXL": "SDXL",
"Image": "画像",
"Upscaling": "アップスケーリング",
"Video": "ビデオ"
},
@@ -1015,13 +1058,14 @@
"flux_redux_model_example": "Flux Reduxモデル",
"flux_schnell": "Flux Schnell"
},
"SD3_5": {
"Image": {
"hidream_i1_dev": "HiDream I1 Dev",
"hidream_i1_fast": "HiDream I1 Fast",
"hidream_i1_full": "HiDream I1 Full",
"sd3_5_large_blur": "SD3.5 ラージブラー",
"sd3_5_large_canny_controlnet_example": "SD3.5 ラージキャニーコントロールネット",
"sd3_5_large_depth": "SD3.5 ラージデプス",
"sd3_5_simple_example": "SD3.5 シンプル"
},
"SDXL": {
"sd3_5_simple_example": "SD3.5 シンプル",
"sdxl_refiner_prompt_example": "SDXL Refinerプロンプト",
"sdxl_revision_text_prompts": "SDXL Revisionテキストプロンプト",
"sdxl_revision_zero_positive": "SDXL Revisionゼロポジティブ",
@@ -1042,7 +1086,9 @@
"ltxv_text_to_video": "LTXVテキストからビデオへ",
"mochi_text_to_video_example": "Mochiテキストからビデオへ",
"text_to_video_wan": "Wan 2.1 テキストからビデオへ",
"txt_to_image_to_video": "テキストから画像へ、画像からビデオへ"
"txt_to_image_to_video": "テキストから画像へ、画像からビデオへ",
"wan2_1_fun_control": "Wan 2.1 ControlNet",
"wan2_1_fun_inp": "Wan 2.1 インペインティング"
}
},
"title": "テンプレートを利用して開始"
@@ -1079,6 +1125,17 @@
"next": "次へ",
"selectUser": "ユーザーを選択"
},
"userSettings": {
"email": "メールアドレス",
"name": "名前",
"notSet": "未設定",
"provider": "サインイン方法",
"providers": {
"github": "GitHub",
"google": "Google"
},
"title": "ユーザー設定"
},
"validation": {
"invalidEmail": "無効なメールアドレス",
"maxLength": "{length}文字以下でなければなりません",

View File

@@ -108,6 +108,10 @@
"Straight": "ストレート"
}
},
"Comfy_Load3D_BackgroundColor": {
"name": "初期背景色",
"tooltip": "3Dシーンのデフォルト背景色を設定します。この設定は新しい3Dウィジェット作成時の背景の見た目を決定しますが、作成後に各ウィジェットごとに個別に調整できます。"
},
"Comfy_Load3D_CameraType": {
"name": "カメラタイプ",
"options": {
@@ -116,6 +120,22 @@
},
"tooltip": "新しい3Dウィジェットが作成されたときに、デフォルトでカメラが透視投影か平行投影かを制御します。このデフォルトは、作成後に各ウィジェットごとに個別に切り替えることができます。"
},
"Comfy_Load3D_LightAdjustmentIncrement": {
"name": "ライト調整増分",
"tooltip": "3Dシーンでライトの強度を調整する際の増分サイズを制御します。ステップ値が小さいほど、照明調整をより細かく制御でき、大きい値では1回の調整ごとにより顕著な変化が得られます。"
},
"Comfy_Load3D_LightIntensity": {
"name": "初期ライト強度",
"tooltip": "3Dシーン内の照明のデフォルトの明るさレベルを設定します。この値は新しい3Dウィジェット作成時にライトがオブジェクトをどれだけ強く照らすかを決定しますが、作成後に各ウィジェットごとに個別に調整できます。"
},
"Comfy_Load3D_LightIntensityMaximum": {
"name": "最大光度",
"tooltip": "3Dシーンで許可される最大光度値を設定します。これは、3Dウィジェットで照明を調整する際に設定できる明るさの上限を定義します。"
},
"Comfy_Load3D_LightIntensityMinimum": {
"name": "光の強度の最小値",
"tooltip": "3Dシーンで許可される光の強度の最小値を設定します。これは、3Dウィジェットで照明を調整する際に設定できる明るさの下限を定義します。"
},
"Comfy_Load3D_ShowGrid": {
"name": "グリッドを表示",
"tooltip": "デフォルトでグリッドを表示するには切り替えます"

View File

@@ -1,4 +1,7 @@
{
"Comfy-Desktop_CheckForUpdates": {
"label": "업데이트 확인"
},
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
"label": "사용자 정의 노드 폴더 열기"
},
@@ -170,6 +173,9 @@
"Comfy_Undo": {
"label": "실행 취소"
},
"Comfy_User_OpenSignInDialog": {
"label": "로그인 대화상자 열기"
},
"Workspace_CloseWorkflow": {
"label": "현재 워크플로우 닫기"
},

View File

@@ -17,6 +17,7 @@
"emailPlaceholder": "이메일을 입력하세요",
"failed": "로그인 실패",
"forgotPassword": "비밀번호를 잊으셨나요?",
"genericErrorMessage": "죄송합니다. 오류가 발생했습니다. {supportEmail}로 문의해 주세요.",
"loginButton": "로그인",
"loginWithGithub": "Github로 로그인",
"loginWithGoogle": "구글로 로그인",
@@ -25,12 +26,32 @@
"passwordLabel": "비밀번호",
"passwordPlaceholder": "비밀번호를 입력하세요",
"privacyLink": "개인정보 보호정책",
"signInOrSignUp": "로그인 / 회원가입",
"signUp": "가입하기",
"success": "로그인 성공",
"termsLink": "이용 약관",
"termsText": "\"다음\" 또는 \"가입하기\"를 클릭하면 우리의",
"title": "계정에 로그인"
},
"required": {
"credits": {
"action": "설정에서 크레딧 추가 열기",
"hint": "크레딧을 추가하려면: 설정 > 사용자 > 크레딧 으로 이동하세요.",
"message": "이 워크플로에는 크레딧이 필요한 노드가 포함되어 있습니다. 계속하려면 계정에 크레딧을 추가하세요.",
"title": "워크플로 실행을 위한 크레딧 필요"
},
"signIn": {
"action": "설정에서 로그인 열기",
"hint": "로그인하려면: 설정 > 사용자 > 로그인 으로 이동하세요.",
"message": "이 워크플로에는 활성 계정이 필요한 노드가 포함되어 있습니다. 계속하려면 로그인하거나 계정을 생성하세요.",
"title": "워크플로 실행을 위한 로그인 필요"
}
},
"signOut": {
"signOut": "로그아웃",
"success": "성공적으로 로그아웃되었습니다",
"successDetail": "계정에서 로그아웃되었습니다."
},
"signup": {
"alreadyHaveAccount": "이미 계정이 있으신가요?",
"emailLabel": "이메일",
@@ -92,6 +113,23 @@
"Title": "제목",
"Unpin": "고정 해제"
},
"credits": {
"credits": "크레딧",
"creditsHistory": "크레딧 내역",
"faqs": "자주 묻는 질문",
"lastUpdated": "마지막 업데이트",
"messageSupport": "지원 문의",
"purchaseCredits": "크레딧 구매",
"topUp": {
"addCredits": "잔액에 크레딧 추가",
"buyNow": "지금 구매",
"insufficientMessage": "이 워크플로우를 실행하기에 크레딧이 부족합니다.",
"insufficientTitle": "크레딧 부족",
"maxAmount": "(최대 $1,000 USD)",
"title": "크레딧 잔액 충전"
},
"yourCreditBalance": "보유 크레딧 잔액"
},
"dataTypes": {
"AUDIO": "오디오",
"BOOLEAN": "논리값",
@@ -137,7 +175,9 @@
"desktopUpdate": {
"description": "ComfyUI 데스크톱이 새로운 종속성을 설치하고 있습니다. 이 작업은 몇 분 정도 걸릴 수 있습니다.",
"terminalDefaultMessage": "업데이트 콘솔 출력은 여기에 표시됩니다.",
"title": "ComfyUI 데스크톱 업데이트 중"
"title": "ComfyUI 데스크톱 업데이트 중",
"updateAvailableMessage": "업데이트가 가능합니다. 지금 재시작하고 업데이트하시겠습니까?",
"updateFoundTitle": "업데이트 발견 (v{version})"
},
"downloadGit": {
"gitWebsite": "git 프로그램 다운로드",
@@ -166,6 +206,7 @@
"about": "정보",
"add": "추가",
"all": "모두",
"amount": "수량",
"apply": "적용",
"back": "뒤로",
"cancel": "취소",
@@ -565,6 +606,7 @@
"Bypass/Unbypass Selected Nodes": "선택한 노드 우회/우회 해제",
"Canvas Toggle Link Visibility": "캔버스 토글 링크 가시성",
"Canvas Toggle Lock": "캔버스 토글 잠금",
"Check for Updates": "업데이트 확인",
"Clear Pending Tasks": "보류 중인 작업 제거하기",
"Clear Workflow": "워크플로 지우기",
"Clipspace": "클립스페이스",
@@ -600,6 +642,7 @@
"Open Logs Folder": "로그 폴더 열기",
"Open Models Folder": "모델 폴더 열기",
"Open Outputs Folder": "출력 폴더 열기",
"Open Sign In Dialog": "로그인 대화 상자 열기",
"Open extra_model_paths_yaml": "extra_model_paths.yaml 열기",
"Pin/Unpin Selected Items": "선택한 항목 고정/고정 해제",
"Pin/Unpin Selected Nodes": "선택한 노드 고정/고정 해제",
@@ -877,6 +920,7 @@
"Graph": "그래프",
"Group": "그룹",
"Keybinding": "키 바인딩",
"Light": "라이트",
"Link": "링크",
"LinkRelease": "링크 해제",
"LiteGraph": "LiteGraph",
@@ -969,8 +1013,7 @@
"ControlNet": "컨트롤넷",
"Custom Nodes": "사용자 정의 노드",
"Flux": "FLUX",
"SD3_5": "SD3.5",
"SDXL": "SDXL",
"Image": "이미지",
"Upscaling": "업스케일링",
"Video": "비디오"
},
@@ -1015,13 +1058,14 @@
"flux_redux_model_example": "FLUX Redux 모델 예제",
"flux_schnell": "FLUX Schnell"
},
"SD3_5": {
"Image": {
"hidream_i1_dev": "HiDream I1 Dev",
"hidream_i1_fast": "HiDream I1 Fast",
"hidream_i1_full": "HiDream I1 Full",
"sd3_5_large_blur": "SD3.5 Large 블러 컨트롤넷",
"sd3_5_large_canny_controlnet_example": "SD3.5 Large 캐니 컨트롤넷",
"sd3_5_large_depth": "SD3.5 Large 깊이 컨트롤넷",
"sd3_5_simple_example": "간단한 SD3.5 예제"
},
"SDXL": {
"sd3_5_simple_example": "간단한 SD3.5 예제",
"sdxl_refiner_prompt_example": "SDXL 리파이너 프롬프트",
"sdxl_revision_text_prompts": "SDXL Revision 텍스트 프롬프트",
"sdxl_revision_zero_positive": "SDXL Revision Zero Positive",
@@ -1042,7 +1086,9 @@
"ltxv_text_to_video": "텍스트 -> 동영상 (LTXV)",
"mochi_text_to_video_example": "텍스트 -> 동영상 (Mochi)",
"text_to_video_wan": "Wan 2.1 텍스트를 비디오로",
"txt_to_image_to_video": "텍스트 -> 이미지 -> 동영상"
"txt_to_image_to_video": "텍스트 -> 이미지 -> 동영상",
"wan2_1_fun_control": "Wan 2.1 컨트롤넷",
"wan2_1_fun_inp": "Wan 2.1 인페인트"
}
},
"title": "템플릿으로 시작하기"
@@ -1079,6 +1125,17 @@
"next": "다음",
"selectUser": "사용자 선택"
},
"userSettings": {
"email": "이메일",
"name": "이름",
"notSet": "설정되지 않음",
"provider": "로그인 방법",
"providers": {
"github": "GitHub",
"google": "Google"
},
"title": "사용자 설정"
},
"validation": {
"invalidEmail": "유효하지 않은 이메일 주소",
"maxLength": "{length}자를 초과할 수 없습니다",

View File

@@ -108,6 +108,10 @@
"Straight": "직선"
}
},
"Comfy_Load3D_BackgroundColor": {
"name": "초기 배경색",
"tooltip": "3D 장면의 기본 배경색을 설정합니다. 이 설정은 새 3D 위젯이 생성될 때 배경의 모양을 결정하지만, 생성 후 각 위젯별로 개별적으로 조정할 수 있습니다."
},
"Comfy_Load3D_CameraType": {
"name": "카메라 유형",
"options": {
@@ -116,6 +120,22 @@
},
"tooltip": "새로운 3D 위젯이 생성될 때 카메라가 기본적으로 원근법 또는 직교법을 사용하는지를 제어합니다. 이 기본값은 생성 후 각 위젯별로 개별적으로 전환할 수 있습니다."
},
"Comfy_Load3D_LightAdjustmentIncrement": {
"name": "조명 조정 증가량",
"tooltip": "3D 장면에서 조명 강도를 조정할 때 증가하는 크기를 제어합니다. 값이 작을수록 조명을 더 세밀하게 조정할 수 있고, 값이 클수록 한 번에 더 눈에 띄는 변화가 발생합니다."
},
"Comfy_Load3D_LightIntensity": {
"name": "초기 조명 강도",
"tooltip": "3D 장면에서 조명의 기본 밝기 수준을 설정합니다. 이 값은 새 3D 위젯이 생성될 때 조명이 오브젝트를 얼마나 강하게 비추는지 결정하지만, 생성 후 각 위젯별로 개별적으로 조정할 수 있습니다."
},
"Comfy_Load3D_LightIntensityMaximum": {
"name": "최대 조명 강도",
"tooltip": "3D 장면에서 허용되는 최대 조명 강도 값을 설정합니다. 이는 모든 3D 위젯에서 조명을 조정할 때 설정할 수 있는 밝기의 상한선을 정의합니다."
},
"Comfy_Load3D_LightIntensityMinimum": {
"name": "최소 광원 세기",
"tooltip": "3D 장면에서 허용되는 최소 광원 세기 값을 설정합니다. 이는 모든 3D 위젯에서 조명을 조정할 때 설정할 수 있는 밝기의 하한을 정의합니다."
},
"Comfy_Load3D_ShowGrid": {
"name": "그리드 표시",
"tooltip": "기본적으로 그리드를 표시하도록 전환"

View File

@@ -1,4 +1,7 @@
{
"Comfy-Desktop_CheckForUpdates": {
"label": "Проверить наличие обновлений"
},
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
"label": "Открыть папку пользовательских нод"
},
@@ -170,6 +173,9 @@
"Comfy_Undo": {
"label": "Отменить"
},
"Comfy_User_OpenSignInDialog": {
"label": "Открыть окно входа"
},
"Workspace_CloseWorkflow": {
"label": "Закрыть текущий рабочий процесс"
},

View File

@@ -17,6 +17,7 @@
"emailPlaceholder": "Введите вашу электронную почту",
"failed": "Вход не удался",
"forgotPassword": "Забыли пароль?",
"genericErrorMessage": "Извините, произошла ошибка. Пожалуйста, свяжитесь с {supportEmail}.",
"loginButton": "Войти",
"loginWithGithub": "Войти через Github",
"loginWithGoogle": "Войти через Google",
@@ -25,12 +26,32 @@
"passwordLabel": "Пароль",
"passwordPlaceholder": "Введите ваш пароль",
"privacyLink": "Политикой конфиденциальности",
"signInOrSignUp": "Войти / Зарегистрироваться",
"signUp": "Зарегистрироваться",
"success": "Вход выполнен успешно",
"termsLink": "Условиями использования",
"termsText": "Нажимая \"Далее\" или \"Зарегистрироваться\", вы соглашаетесь с нашими",
"title": "Войдите в свой аккаунт"
},
"required": {
"credits": {
"action": "Открыть настройки для пополнения кредитов",
"hint": "Чтобы пополнить кредиты, перейдите в: Настройки > Пользователь > Кредиты",
"message": "Этот рабочий процесс содержит узлы, для которых необходимы кредиты. Пожалуйста, пополните баланс, чтобы продолжить.",
"title": "Требуются кредиты для выполнения рабочего процесса"
},
"signIn": {
"action": "Открыть настройки для входа",
"hint": "Чтобы войти, перейдите в: Настройки > Пользователь > Вход",
"message": "Этот рабочий процесс содержит узлы, для которых необходима активная учетная запись. Пожалуйста, войдите или создайте учетную запись, чтобы продолжить.",
"title": "Требуется вход для выполнения рабочего процесса"
}
},
"signOut": {
"signOut": "Выйти",
"success": "Вы успешно вышли из системы",
"successDetail": "Вы вышли из своей учетной записи."
},
"signup": {
"alreadyHaveAccount": "Уже есть аккаунт?",
"emailLabel": "Электронная почта",
@@ -92,6 +113,23 @@
"Title": "Заголовок",
"Unpin": "Открепить"
},
"credits": {
"credits": "Кредиты",
"creditsHistory": "История кредитов",
"faqs": "Часто задаваемые вопросы",
"lastUpdated": "Последнее обновление",
"messageSupport": "Связаться с поддержкой",
"purchaseCredits": "Купить кредиты",
"topUp": {
"addCredits": "Добавить кредиты на баланс",
"buyNow": "Купить сейчас",
"insufficientMessage": "У вас недостаточно кредитов для запуска этого рабочего процесса.",
"insufficientTitle": "Недостаточно кредитов",
"maxAmount": "(Макс. $1,000 USD)",
"title": "Пополнить баланс кредитов"
},
"yourCreditBalance": "Ваш баланс кредитов"
},
"dataTypes": {
"AUDIO": "АУДИО",
"BOOLEAN": "БУЛЕВО",
@@ -137,7 +175,9 @@
"desktopUpdate": {
"description": "ComfyUI Desktop устанавливает новые зависимости. Это может занять несколько минут.",
"terminalDefaultMessage": "Любой вывод консоли из обновления будет отображаться здесь.",
"title": "Обновление ComfyUI Desktop"
"title": "Обновление ComfyUI Desktop",
"updateAvailableMessage": "Доступно обновление. Вы хотите перезагрузить и обновить сейчас?",
"updateFoundTitle": "Найдено обновление (v{version})"
},
"downloadGit": {
"gitWebsite": "Скачать git",
@@ -166,6 +206,7 @@
"about": "О программе",
"add": "Добавить",
"all": "Все",
"amount": "Количество",
"apply": "Применить",
"back": "Назад",
"cancel": "Отмена",
@@ -565,6 +606,7 @@
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные ноды",
"Canvas Toggle Link Visibility": "Переключение видимости ссылки на холст",
"Canvas Toggle Lock": "Переключение блокировки холста",
"Check for Updates": "Проверить наличие обновлений",
"Clear Pending Tasks": "Очистить ожидающие задачи",
"Clear Workflow": "Очистить рабочий процесс",
"Clipspace": "Клиппространство",
@@ -600,6 +642,7 @@
"Open Logs Folder": "Открыть папку журналов",
"Open Models Folder": "Открыть папку моделей",
"Open Outputs Folder": "Открыть папку выходных данных",
"Open Sign In Dialog": "Открыть окно входа",
"Open extra_model_paths_yaml": "Открыть extra_model_paths.yaml",
"Pin/Unpin Selected Items": "Закрепить/открепить выбранные элементы",
"Pin/Unpin Selected Nodes": "Закрепить/открепить выбранные ноды",
@@ -877,6 +920,7 @@
"Graph": "Граф",
"Group": "Группа",
"Keybinding": "Сочетание клавиш",
"Light": "Светлый",
"Link": "Ссылка",
"LinkRelease": "Освобождение ссылки",
"LiteGraph": "Lite Graph",
@@ -969,8 +1013,7 @@
"ControlNet": "ControlNet",
"Custom Nodes": "Пользовательские узлы",
"Flux": "Flux",
"SD3_5": "SD3.5",
"SDXL": "SDXL",
"Image": "Изображение",
"Upscaling": "Увеличение разрешения",
"Video": "Видео"
},
@@ -1015,17 +1058,18 @@
"flux_redux_model_example": "Flux Redux Model",
"flux_schnell": "Flux Schnell"
},
"SD3_5": {
"Image": {
"hidream_i1_dev": "HiDream I1 Dev",
"hidream_i1_fast": "HiDream I1 Fast",
"hidream_i1_full": "HiDream I1 Full",
"sd3_5_large_blur": "SD3.5 Большое размытие",
"sd3_5_large_canny_controlnet_example": "SD3.5 Большой Canny ControlNet",
"sd3_5_large_depth": "SD3.5 Большая глубина",
"sd3_5_simple_example": "SD3.5 Простой"
},
"SDXL": {
"sdxl_refiner_prompt_example": "SDXL Refiner Prompt",
"sdxl_revision_text_prompts": "SDXL Revision Text Prompts",
"sdxl_revision_zero_positive": "SDXL Revision Zero Positive",
"sdxl_simple_example": "SDXL Simple",
"sd3_5_simple_example": "SD3.5 Простой",
"sdxl_refiner_prompt_example": "SDXL Уточняющий запрос",
"sdxl_revision_text_prompts": "SDXL Редактирование текстовых запросов",
"sdxl_revision_zero_positive": "SDXL Редактирование нулевого положительного",
"sdxl_simple_example": "SDXL Простой",
"sdxlturbo_example": "SDXL Turbo"
},
"Upscaling": {
@@ -1042,7 +1086,9 @@
"ltxv_text_to_video": "LTXV Text to Video",
"mochi_text_to_video_example": "Mochi Text to Video",
"text_to_video_wan": "Wan 2.1 Текст в Видео",
"txt_to_image_to_video": "Текст в изображение в видео"
"txt_to_image_to_video": "Текст в изображение в видео",
"wan2_1_fun_control": "Wan 2.1 ControlNet",
"wan2_1_fun_inp": "Wan 2.1 Inpainting"
}
},
"title": "Начните с шаблона"
@@ -1079,6 +1125,17 @@
"next": "Далее",
"selectUser": "Выберите пользователя"
},
"userSettings": {
"email": "Электронная почта",
"name": "Имя",
"notSet": "Не задано",
"provider": "Способ входа",
"providers": {
"github": "GitHub",
"google": "Google"
},
"title": "Настройки пользователя"
},
"validation": {
"invalidEmail": "Недействительный адрес электронной почты",
"maxLength": "Должно быть не более {length} символов",

View File

@@ -108,6 +108,10 @@
"Straight": "Прямой"
}
},
"Comfy_Load3D_BackgroundColor": {
"name": "Начальный цвет фона",
"tooltip": "Управляет цветом фона по умолчанию для 3D-сцены. Этот параметр определяет внешний вид фона при создании нового 3D-виджета, но может быть изменён индивидуально для каждого виджета после создания."
},
"Comfy_Load3D_CameraType": {
"name": "Тип камеры",
"options": {
@@ -116,6 +120,22 @@
},
"tooltip": "Управляет тем, является ли камера перспективной или ортографической по умолчанию при создании нового 3D-виджета. Это значение по умолчанию все еще может быть переключено индивидуально для каждого виджета после его создания."
},
"Comfy_Load3D_LightAdjustmentIncrement": {
"name": "Шаг регулировки освещения",
"tooltip": "Определяет размер шага при регулировке интенсивности освещения в 3D-сценах. Меньшее значение шага позволяет более точно настраивать освещение, а большее значение приводит к более заметным изменениям при каждой регулировке."
},
"Comfy_Load3D_LightIntensity": {
"name": "Начальная интенсивность освещения",
"tooltip": "Устанавливает уровень яркости освещения по умолчанию в 3D-сцене. Это значение определяет, насколько сильно источники света освещают объекты при создании нового 3D-виджета, но может быть изменено для каждого виджета отдельно после создания."
},
"Comfy_Load3D_LightIntensityMaximum": {
"name": "Максимальная интенсивность света",
"tooltip": "Устанавливает максимальное допустимое значение интенсивности света для 3D-сцен. Определяет верхний предел яркости, который можно задать при настройке освещения в любом 3D-виджете."
},
"Comfy_Load3D_LightIntensityMinimum": {
"name": "Минимальная интенсивность света",
"tooltip": "Устанавливает минимально допустимое значение интенсивности света для 3D-сцен. Определяет нижний предел яркости, который можно установить при настройке освещения в любом 3D-виджете."
},
"Comfy_Load3D_ShowGrid": {
"name": "Показать сетку",
"tooltip": "Переключиться, чтобы показывать сетку по умолчанию"

View File

@@ -1,4 +1,7 @@
{
"Comfy-Desktop_CheckForUpdates": {
"label": "检查更新"
},
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
"label": "打开自定义节点文件夹"
},
@@ -170,6 +173,9 @@
"Comfy_Undo": {
"label": "撤销"
},
"Comfy_User_OpenSignInDialog": {
"label": "打开登录对话框"
},
"Workspace_CloseWorkflow": {
"label": "关闭当前工作流"
},

View File

@@ -17,6 +17,7 @@
"emailPlaceholder": "输入您的电子邮件",
"failed": "登录失败",
"forgotPassword": "忘记密码?",
"genericErrorMessage": "抱歉,我们遇到了一些错误。请联系 {supportEmail}。",
"loginButton": "登录",
"loginWithGithub": "使用Github登录",
"loginWithGoogle": "使用Google登录",
@@ -25,12 +26,32 @@
"passwordLabel": "密码",
"passwordPlaceholder": "输入您的密码",
"privacyLink": "隐私政策",
"signInOrSignUp": "登录 / 注册",
"signUp": "注册",
"success": "登录成功",
"termsLink": "使用条款",
"termsText": "点击“下一步”或“注册”即表示您同意我们的",
"title": "登录您的账户"
},
"required": {
"credits": {
"action": "打开设置进行充值",
"hint": "要充值,请前往:设置 > 用户 > 积分",
"message": "此工作流包含需要积分的节点。请为您的账户充值以继续。",
"title": "需要积分以执行工作流"
},
"signIn": {
"action": "打开设置进行登录",
"hint": "要登录,请前往:设置 > 用户 > 登录",
"message": "此工作流包含需要活跃账户的节点。请登录或创建账户以继续。",
"title": "需要登录以执行工作流"
}
},
"signOut": {
"signOut": "退出登录",
"success": "成功退出登录",
"successDetail": "您已成功退出账户。"
},
"signup": {
"alreadyHaveAccount": "已经有账户了?",
"emailLabel": "电子邮件",
@@ -92,6 +113,23 @@
"Title": "标题",
"Unpin": "取消固定"
},
"credits": {
"credits": "积分",
"creditsHistory": "积分历史",
"faqs": "常见问题",
"lastUpdated": "最近更新",
"messageSupport": "联系客服",
"purchaseCredits": "购买积分",
"topUp": {
"addCredits": "为您的余额充值",
"buyNow": "立即购买",
"insufficientMessage": "您的积分不足,无法运行此工作流。",
"insufficientTitle": "积分不足",
"maxAmount": "(最高 $1,000 美元)",
"title": "充值余额"
},
"yourCreditBalance": "您的积分余额"
},
"dataTypes": {
"AUDIO": "音频",
"BOOLEAN": "布尔",
@@ -137,7 +175,9 @@
"desktopUpdate": {
"description": "ComfyUI桌面正在安装新的依赖项。这可能需要几分钟的时间。",
"terminalDefaultMessage": "更新过程中的任何控制台输出都将在这里显示。",
"title": "正在更新ComfyUI桌面"
"title": "正在更新ComfyUI桌面",
"updateAvailableMessage": "有可用的更新。您现在要重启并更新吗?",
"updateFoundTitle": "找到更新 (v{version})"
},
"downloadGit": {
"gitWebsite": "下载 git",
@@ -166,6 +206,7 @@
"about": "关于",
"add": "添加",
"all": "全部",
"amount": "数量",
"apply": "应用",
"back": "返回",
"cancel": "取消",
@@ -565,6 +606,7 @@
"Bypass/Unbypass Selected Nodes": "忽略/取消忽略选定节点",
"Canvas Toggle Link Visibility": "切换连线可见性",
"Canvas Toggle Lock": "切换视图锁定",
"Check for Updates": "检查更新",
"Clear Pending Tasks": "清除待处理任务",
"Clear Workflow": "清除工作流",
"Clipspace": "剪贴空间",
@@ -600,6 +642,7 @@
"Open Logs Folder": "打开日志文件夹",
"Open Models Folder": "打开模型文件夹",
"Open Outputs Folder": "打开输出文件夹",
"Open Sign In Dialog": "打开登录对话框",
"Open extra_model_paths_yaml": "打开 extra_model_paths.yaml",
"Pin/Unpin Selected Items": "固定/取消固定选定项目",
"Pin/Unpin Selected Nodes": "固定/取消固定选定节点",
@@ -877,6 +920,7 @@
"Graph": "画面",
"Group": "组节点",
"Keybinding": "快捷键",
"Light": "浅色",
"Link": "连线",
"LinkRelease": "释放链接",
"LiteGraph": "画面",
@@ -969,8 +1013,7 @@
"ControlNet": "ControlNet",
"Custom Nodes": "自定义节点",
"Flux": "Flux",
"SD3_5": "SD3.5",
"SDXL": "SDXL",
"Image": "图片",
"Upscaling": "放大",
"Video": "视频"
},
@@ -1000,7 +1043,7 @@
"lora_multiple": "Lora多个"
},
"ControlNet": {
"2_pass_pose_worship": "2通道姿势崇拜",
"2_pass_pose_worship": "通道姿势处理",
"controlnet_example": "ControlNet",
"depth_controlnet": "深度ControlNet",
"depth_t2i_adapter": "深度T2I适配器",
@@ -1015,13 +1058,14 @@
"flux_redux_model_example": "Flux Redux Model",
"flux_schnell": "Flux Schnell"
},
"SD3_5": {
"Image": {
"hidream_i1_dev": "HiDream I1 Dev",
"hidream_i1_fast": "HiDream I1 Fast",
"hidream_i1_full": "HiDream I1 Full",
"sd3_5_large_blur": "SD3.5 Large 模糊",
"sd3_5_large_canny_controlnet_example": "SD3.5 Large Canny 控制网",
"sd3_5_large_depth": "SD3.5 Large 深度",
"sd3_5_simple_example": "SD3.5 简易示例"
},
"SDXL": {
"sd3_5_simple_example": "SD3.5 简易示例",
"sdxl_refiner_prompt_example": "SDXL Refiner提示",
"sdxl_revision_text_prompts": "SDXL修订文本提示",
"sdxl_revision_zero_positive": "SDXL修订零正",
@@ -1042,7 +1086,9 @@
"ltxv_text_to_video": "LTXV文本到视频",
"mochi_text_to_video_example": "Mochi文本到视频",
"text_to_video_wan": "Wan 2.1 文字到视频",
"txt_to_image_to_video": "文本到图像到视频"
"txt_to_image_to_video": "文本到图像到视频",
"wan2_1_fun_control": "Wan 2.1 ControlNet",
"wan2_1_fun_inp": "Wan 2.1 图像修复"
}
},
"title": "从模板开始"
@@ -1079,6 +1125,17 @@
"next": "下一步",
"selectUser": "选择用户"
},
"userSettings": {
"email": "电子邮件",
"name": "名称",
"notSet": "未设置",
"provider": "登录方式",
"providers": {
"github": "GitHub",
"google": "Google"
},
"title": "用户设置"
},
"validation": {
"invalidEmail": "无效的电子邮件地址",
"maxLength": "不能超过{length}个字符",

View File

@@ -108,6 +108,10 @@
"Straight": "直角线"
}
},
"Comfy_Load3D_BackgroundColor": {
"name": "初始背景颜色",
"tooltip": "控制3D场景的默认背景颜色。此设置决定新建3D组件时的背景外观但每个组件在创建后都可以单独调整。"
},
"Comfy_Load3D_CameraType": {
"name": "摄像机类型",
"options": {
@@ -116,6 +120,22 @@
},
"tooltip": "控制创建新的3D小部件时默认的相机是透视还是正交。这个默认设置仍然可以在创建后为每个小部件单独切换。"
},
"Comfy_Load3D_LightAdjustmentIncrement": {
"name": "光照调整步长",
"tooltip": "控制在3D场景中调整光照强度时的步长。较小的步长值可以实现更精细的光照调整较大的值则会使每次调整的变化更加明显。"
},
"Comfy_Load3D_LightIntensity": {
"name": "初始光照强度",
"tooltip": "设置3D场景中灯光的默认亮度级别。该数值决定新建3D控件时灯光照亮物体的强度但每个控件在创建后都可以单独调整。"
},
"Comfy_Load3D_LightIntensityMaximum": {
"name": "最大光照强度",
"tooltip": "设置3D场景允许的最大光照强度值。此项定义了在调整任何3D控件照明时可设定的最高亮度上限。"
},
"Comfy_Load3D_LightIntensityMinimum": {
"name": "光照强度下限",
"tooltip": "设置3D场景允许的最小光照强度值。此项定义在调整任何3D控件照明时可设定的最低亮度。"
},
"Comfy_Load3D_ShowGrid": {
"name": "显示网格",
"tooltip": "默认显示网格开关"

View File

@@ -442,6 +442,11 @@ const zSettings = z.object({
'Comfy.MaskEditor.UseDominantAxis': z.boolean(),
'Comfy.Load3D.ShowGrid': z.boolean(),
'Comfy.Load3D.ShowPreview': z.boolean(),
'Comfy.Load3D.BackgroundColor': z.string(),
'Comfy.Load3D.LightIntensity': z.number(),
'Comfy.Load3D.LightIntensityMaximum': z.number(),
'Comfy.Load3D.LightIntensityMinimum': z.number(),
'Comfy.Load3D.LightAdjustmentIncrement': z.number(),
'Comfy.Load3D.CameraType': z.enum(['perspective', 'orthographic']),
'pysssss.SnapToGrid': z.boolean(),
/** VHS setting is used for queue video preview support. */

View File

@@ -160,12 +160,29 @@ const zAuxId = z
)
.transform(([username, repo]) => `${username}/${repo}`)
const zSemVer = z.union([
z.string().regex(semverPattern, 'Invalid semantic version (x.y.z)'),
const zGitHash = z.string().superRefine((val: string, ctx) => {
if (!gitHashPattern.test(val)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Node pack version has invalid Git commit hash: "${val}"`
})
}
})
const zSemVer = z.string().superRefine((val: string, ctx) => {
if (!semverPattern.test(val)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Node pack version has invalid semantic version: "${val}"`
})
}
})
const zVersion = z.union([
z
.string()
.transform((ver) => ver.replace(/^v/, '')) // Strip leading 'v'
.pipe(z.union([zSemVer, zGitHash])),
z.literal('unknown')
])
const zGitHash = z.string().regex(gitHashPattern, 'Invalid Git commit hash')
const zVersion = z.union([zSemVer, zGitHash])
const zProperties = z
.object({

View File

@@ -74,6 +74,7 @@ export const zComboInputOptions = zBaseInputOptions.extend({
image_folder: z.enum(['input', 'output', 'temp']).optional(),
allow_batch: z.boolean().optional(),
video_upload: z.boolean().optional(),
animated_image_upload: z.boolean().optional(),
options: z.array(zComboOption).optional(),
remote: zRemoteWidgetConfig.optional(),
/** Whether the widget is a multi-select widget. */

View File

@@ -24,7 +24,11 @@ import type {
User,
UserDataFullInfo
} from '@/schemas/apiSchema'
import type { ComfyWorkflowJSON, NodeId } from '@/schemas/comfyWorkflowSchema'
import type {
ComfyApiWorkflow,
ComfyWorkflowJSON,
NodeId
} from '@/schemas/comfyWorkflowSchema'
import {
type ComfyNodeDef,
validateComfyNodeDef
@@ -33,13 +37,27 @@ import { WorkflowTemplates } from '@/types/workflowTemplateTypes'
interface QueuePromptRequestBody {
client_id: string
// Mapping from node id to node info + input values
// TODO: Type this.
prompt: Record<number, any>
prompt: ComfyApiWorkflow
extra_data: {
extra_pnginfo: {
workflow: ComfyWorkflowJSON
}
/**
* The auth token for the comfy org account if the user is logged in.
*
* Backend node can access this token by specifying following input:
* ```python
@classmethod
def INPUT_TYPES(s):
return {
"hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG"}
}
def execute(self, auth_token: str):
print(f"Auth token: {auth_token}")
* ```
*/
auth_token_comfy_org?: string
}
front?: boolean
number?: number
@@ -499,19 +517,23 @@ export class ComfyApi extends EventTarget {
* Queues a prompt to be executed
* @param {number} number The index at which to queue the prompt, passing -1 will insert the prompt at the front of the queue
* @param {object} prompt The prompt data to queue
* @param {string} authToken The auth token for the comfy org account if the user is logged in
* @throws {PromptExecutionError} If the prompt fails to execute
*/
async queuePrompt(
number: number,
{
output,
workflow
}: { output: Record<number, any>; workflow: ComfyWorkflowJSON }
data: { output: ComfyApiWorkflow; workflow: ComfyWorkflowJSON },
authToken?: string
): Promise<PromptResponse> {
const { output: prompt, workflow } = data
const body: QueuePromptRequestBody = {
client_id: this.clientId ?? '', // TODO: Unify clientId access
prompt: output,
extra_data: { extra_pnginfo: { workflow } }
prompt,
extra_data: {
auth_token_comfy_org: authToken,
extra_pnginfo: { workflow }
}
}
if (number === -1) {

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