Compare commits

...

51 Commits

Author SHA1 Message Date
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
99 changed files with 6395 additions and 744 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()

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

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.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@comfyorg/comfyui-frontend",
"version": "1.17.0",
"version": "1.17.5",
"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.5",
"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,7 @@
</PanelTemplate>
<AboutPanel />
<CreditsPanel />
<Suspense>
<KeybindingPanel />
<template #fallback>
@@ -73,29 +80,24 @@ 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'
const props = defineProps<{
const { defaultPanel } = defineProps<{
defaultPanel?: 'about' | 'keybinding' | 'extension' | 'server-config'
}>()
@@ -109,71 +111,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 +137,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 +195,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-policy"
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

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

@@ -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,130 @@
<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">{{ creditBalance }}</div>
</div>
<Button :label="$t('credits.purchaseCredits')" />
</div>
</div>
<Divider class="mt-12" />
<div class="flex justify-between items-center">
<h3 class="text-base font-medium">
{{ $t('credits.creditsHistory') }}
</h3>
<Button
:label="$t('credits.paymentDetails')"
text
severity="secondary"
icon="pi pi-arrow-up-right"
/>
</div>
<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 ? '+' : '-' }}{{ data.amount }}
</div>
</template>
</Column>
</DataTable>
</div>
<Divider />
<div class="flex flex-row gap-2">
<Button
:label="$t('credits.faqs')"
text
severity="secondary"
icon="pi pi-question-circle"
/>
<Button
:label="$t('credits.messageSupport')"
text
severity="secondary"
icon="pi pi-comments"
/>
</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 TabPanel from 'primevue/tabpanel'
import Tag from 'primevue/tag'
import { ref } from 'vue'
// Mock data - in a real implementation, this would come from a store or API
const creditBalance = ref(0.05)
interface CreditHistoryItemData {
title: string
timestamp: string
amount: number
isPositive: boolean
}
const creditHistory = ref<CreditHistoryItemData[]>([
{
title: 'Kling Text-to-Video v1-6',
timestamp: '2025-04-09, 12:50:08 p.m.',
amount: 4,
isPositive: false
},
{
title: 'Kling Text-to-Video v1-6',
timestamp: '2025-04-09, 12:50:08 p.m.',
amount: 23,
isPositive: false
},
{
title: 'Kling Text-to-Video v1-6',
timestamp: '2025-04-09, 12:50:08 p.m.',
amount: 22,
isPositive: false
},
{
title: 'Free monthly credits',
timestamp: '2025-04-09, 12:46:08 p.m.',
amount: 166,
isPositive: true
}
])
</script>

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

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

@@ -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,141 @@
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'
) {
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 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>(() => {
return defaultPanel
? settingCategories.value.find((x) => x.key === defaultPanel) ??
settingCategories.value[0]
: settingCategories.value[0]
})
const translateCategory = (node: SettingTreeNode) => ({
...node,
translatedLabel: t(
`settingsCategories.${normalizeI18nKey(node.label)}`,
node.label
)
})
const groupedMenuTreeNodes = computed<SettingTreeNode[]>(() => [
// Account settings - only show when user is authenticated
...(firebaseAuthStore.isAuthenticated
? [
{
key: 'account',
label: 'Account',
children: [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

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

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

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

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",
@@ -722,7 +726,8 @@
"Load 3D": "Load 3D",
"Camera": "Camera",
"Scene": "Scene",
"3D": "3D"
"3D": "3D",
"Light": "Light"
},
"serverConfigItems": {
"listen": {
@@ -990,7 +995,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",
@@ -1102,5 +1109,14 @@
"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",
"paymentDetails": "Payment Details",
"faqs": "FAQs",
"messageSupport": "Message Support"
}
}

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

View File

@@ -92,6 +92,15 @@
"Title": "Título",
"Unpin": "Desanclar"
},
"credits": {
"credits": "Créditos",
"creditsHistory": "Historial de créditos",
"faqs": "Preguntas frecuentes",
"messageSupport": "Contactar soporte",
"paymentDetails": "Detalles de pago",
"purchaseCredits": "Comprar créditos",
"yourCreditBalance": "Tu saldo de créditos"
},
"dataTypes": {
"AUDIO": "AUDIO",
"BOOLEAN": "BOOLEANO",
@@ -137,7 +146,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 +177,7 @@
"about": "Acerca de",
"add": "Añadir",
"all": "Todo",
"amount": "Cantidad",
"apply": "Aplicar",
"back": "Atrás",
"cancel": "Cancelar",
@@ -565,6 +577,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",
@@ -877,6 +890,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 +983,7 @@
"ControlNet": "ControlNet",
"Custom Nodes": "Nodos Personalizados",
"Flux": "Flux",
"SD3_5": "SD3.5",
"SDXL": "SDXL",
"Image": "Imagen",
"Upscaling": "Ampliación",
"Video": "Video"
},
@@ -1015,13 +1028,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 +1056,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"

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

View File

@@ -92,6 +92,15 @@
"Title": "Titre",
"Unpin": "Désépingler"
},
"credits": {
"credits": "Crédits",
"creditsHistory": "Historique des crédits",
"faqs": "FAQ",
"messageSupport": "Contacter le support",
"paymentDetails": "Détails de paiement",
"purchaseCredits": "Acheter des crédits",
"yourCreditBalance": "Votre solde de crédits"
},
"dataTypes": {
"AUDIO": "AUDIO",
"BOOLEAN": "BOOLEAN",
@@ -137,7 +146,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 +177,7 @@
"about": "À propos",
"add": "Ajouter",
"all": "Tout",
"amount": "Quantité",
"apply": "Appliquer",
"back": "Retour",
"cancel": "Annuler",
@@ -565,6 +577,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",
@@ -877,6 +890,7 @@
"Graph": "Graphique",
"Group": "Groupe",
"Keybinding": "Raccourci Clavier",
"Light": "Clair",
"Link": "Lien",
"LinkRelease": "Libération de Lien",
"LiteGraph": "Lite Graph",
@@ -969,8 +983,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 +1028,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 +1056,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"

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": "カスタムノードフォルダを開く"
},

View File

@@ -92,6 +92,15 @@
"Title": "タイトル",
"Unpin": "ピンを解除"
},
"credits": {
"credits": "クレジット",
"creditsHistory": "クレジット履歴",
"faqs": "よくある質問",
"messageSupport": "サポートにメッセージ",
"paymentDetails": "支払い詳細",
"purchaseCredits": "クレジットを購入",
"yourCreditBalance": "あなたのクレジット残高"
},
"dataTypes": {
"AUDIO": "オーディオ",
"BOOLEAN": "ブール",
@@ -137,7 +146,9 @@
"desktopUpdate": {
"description": "ComfyUIデスクトップは新しい依存関係をインストールしています。これには数分かかる場合があります。",
"terminalDefaultMessage": "更新からの任意のコンソール出力はここに表示されます。",
"title": "ComfyUIデスクトップの更新"
"title": "ComfyUIデスクトップの更新",
"updateAvailableMessage": "アップデートが利用可能です。今すぐ再起動してアップデートしますか?",
"updateFoundTitle": "アップデートが見つかりました (v{version})"
},
"downloadGit": {
"gitWebsite": "Gitをダウンロード",
@@ -166,6 +177,7 @@
"about": "情報",
"add": "追加",
"all": "すべて",
"amount": "量",
"apply": "適用する",
"back": "戻る",
"cancel": "キャンセル",
@@ -565,6 +577,7 @@
"Bypass/Unbypass Selected Nodes": "選択したノードのバイパス/バイパス解除",
"Canvas Toggle Link Visibility": "キャンバスのリンク表示を切り替え",
"Canvas Toggle Lock": "キャンバスのロックを切り替え",
"Check for Updates": "更新を確認する",
"Clear Pending Tasks": "保留中のタスクをクリア",
"Clear Workflow": "ワークフローをクリア",
"Clipspace": "クリップスペース",
@@ -877,6 +890,7 @@
"Graph": "グラフ",
"Group": "グループ",
"Keybinding": "キー割り当て",
"Light": "ライト",
"Link": "リンク",
"LinkRelease": "リンク解除",
"LiteGraph": "Lite Graph",
@@ -969,8 +983,7 @@
"ControlNet": "ControlNet",
"Custom Nodes": "カスタムノード",
"Flux": "Flux",
"SD3_5": "SD3.5",
"SDXL": "SDXL",
"Image": "画像",
"Upscaling": "アップスケーリング",
"Video": "ビデオ"
},
@@ -1015,13 +1028,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 +1056,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": "テンプレートを利用して開始"

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": "사용자 정의 노드 폴더 열기"
},

View File

@@ -92,6 +92,15 @@
"Title": "제목",
"Unpin": "고정 해제"
},
"credits": {
"credits": "크레딧",
"creditsHistory": "크레딧 내역",
"faqs": "자주 묻는 질문",
"messageSupport": "지원 문의",
"paymentDetails": "결제 정보",
"purchaseCredits": "크레딧 구매",
"yourCreditBalance": "보유 크레딧 잔액"
},
"dataTypes": {
"AUDIO": "오디오",
"BOOLEAN": "논리값",
@@ -137,7 +146,9 @@
"desktopUpdate": {
"description": "ComfyUI 데스크톱이 새로운 종속성을 설치하고 있습니다. 이 작업은 몇 분 정도 걸릴 수 있습니다.",
"terminalDefaultMessage": "업데이트 콘솔 출력은 여기에 표시됩니다.",
"title": "ComfyUI 데스크톱 업데이트 중"
"title": "ComfyUI 데스크톱 업데이트 중",
"updateAvailableMessage": "업데이트가 가능합니다. 지금 재시작하고 업데이트하시겠습니까?",
"updateFoundTitle": "업데이트 발견 (v{version})"
},
"downloadGit": {
"gitWebsite": "git 프로그램 다운로드",
@@ -166,6 +177,7 @@
"about": "정보",
"add": "추가",
"all": "모두",
"amount": "수량",
"apply": "적용",
"back": "뒤로",
"cancel": "취소",
@@ -565,6 +577,7 @@
"Bypass/Unbypass Selected Nodes": "선택한 노드 우회/우회 해제",
"Canvas Toggle Link Visibility": "캔버스 토글 링크 가시성",
"Canvas Toggle Lock": "캔버스 토글 잠금",
"Check for Updates": "업데이트 확인",
"Clear Pending Tasks": "보류 중인 작업 제거하기",
"Clear Workflow": "워크플로 지우기",
"Clipspace": "클립스페이스",
@@ -877,6 +890,7 @@
"Graph": "그래프",
"Group": "그룹",
"Keybinding": "키 바인딩",
"Light": "라이트",
"Link": "링크",
"LinkRelease": "링크 해제",
"LiteGraph": "LiteGraph",
@@ -969,8 +983,7 @@
"ControlNet": "컨트롤넷",
"Custom Nodes": "사용자 정의 노드",
"Flux": "FLUX",
"SD3_5": "SD3.5",
"SDXL": "SDXL",
"Image": "이미지",
"Upscaling": "업스케일링",
"Video": "비디오"
},
@@ -1015,13 +1028,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 +1056,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": "템플릿으로 시작하기"

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": "Открыть папку пользовательских нод"
},

View File

@@ -92,6 +92,15 @@
"Title": "Заголовок",
"Unpin": "Открепить"
},
"credits": {
"credits": "Кредиты",
"creditsHistory": "История кредитов",
"faqs": "Часто задаваемые вопросы",
"messageSupport": "Связаться с поддержкой",
"paymentDetails": "Детали оплаты",
"purchaseCredits": "Купить кредиты",
"yourCreditBalance": "Ваш баланс кредитов"
},
"dataTypes": {
"AUDIO": "АУДИО",
"BOOLEAN": "БУЛЕВО",
@@ -137,7 +146,9 @@
"desktopUpdate": {
"description": "ComfyUI Desktop устанавливает новые зависимости. Это может занять несколько минут.",
"terminalDefaultMessage": "Любой вывод консоли из обновления будет отображаться здесь.",
"title": "Обновление ComfyUI Desktop"
"title": "Обновление ComfyUI Desktop",
"updateAvailableMessage": "Доступно обновление. Вы хотите перезагрузить и обновить сейчас?",
"updateFoundTitle": "Найдено обновление (v{version})"
},
"downloadGit": {
"gitWebsite": "Скачать git",
@@ -166,6 +177,7 @@
"about": "О программе",
"add": "Добавить",
"all": "Все",
"amount": "Количество",
"apply": "Применить",
"back": "Назад",
"cancel": "Отмена",
@@ -565,6 +577,7 @@
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные ноды",
"Canvas Toggle Link Visibility": "Переключение видимости ссылки на холст",
"Canvas Toggle Lock": "Переключение блокировки холста",
"Check for Updates": "Проверить наличие обновлений",
"Clear Pending Tasks": "Очистить ожидающие задачи",
"Clear Workflow": "Очистить рабочий процесс",
"Clipspace": "Клиппространство",
@@ -877,6 +890,7 @@
"Graph": "Граф",
"Group": "Группа",
"Keybinding": "Сочетание клавиш",
"Light": "Светлый",
"Link": "Ссылка",
"LinkRelease": "Освобождение ссылки",
"LiteGraph": "Lite Graph",
@@ -969,8 +983,7 @@
"ControlNet": "ControlNet",
"Custom Nodes": "Пользовательские узлы",
"Flux": "Flux",
"SD3_5": "SD3.5",
"SDXL": "SDXL",
"Image": "Изображение",
"Upscaling": "Увеличение разрешения",
"Video": "Видео"
},
@@ -1015,17 +1028,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 +1056,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": "Начните с шаблона"

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": "打开自定义节点文件夹"
},

View File

@@ -92,6 +92,15 @@
"Title": "标题",
"Unpin": "取消固定"
},
"credits": {
"credits": "积分",
"creditsHistory": "积分历史",
"faqs": "常见问题",
"messageSupport": "联系客服",
"paymentDetails": "支付详情",
"purchaseCredits": "购买积分",
"yourCreditBalance": "您的积分余额"
},
"dataTypes": {
"AUDIO": "音频",
"BOOLEAN": "布尔",
@@ -137,7 +146,9 @@
"desktopUpdate": {
"description": "ComfyUI桌面正在安装新的依赖项。这可能需要几分钟的时间。",
"terminalDefaultMessage": "更新过程中的任何控制台输出都将在这里显示。",
"title": "正在更新ComfyUI桌面"
"title": "正在更新ComfyUI桌面",
"updateAvailableMessage": "有可用的更新。您现在要重启并更新吗?",
"updateFoundTitle": "找到更新 (v{version})"
},
"downloadGit": {
"gitWebsite": "下载 git",
@@ -166,6 +177,7 @@
"about": "关于",
"add": "添加",
"all": "全部",
"amount": "数量",
"apply": "应用",
"back": "返回",
"cancel": "取消",
@@ -565,6 +577,7 @@
"Bypass/Unbypass Selected Nodes": "忽略/取消忽略选定节点",
"Canvas Toggle Link Visibility": "切换连线可见性",
"Canvas Toggle Lock": "切换视图锁定",
"Check for Updates": "检查更新",
"Clear Pending Tasks": "清除待处理任务",
"Clear Workflow": "清除工作流",
"Clipspace": "剪贴空间",
@@ -877,6 +890,7 @@
"Graph": "画面",
"Group": "组节点",
"Keybinding": "快捷键",
"Light": "浅色",
"Link": "连线",
"LinkRelease": "释放链接",
"LiteGraph": "画面",
@@ -969,8 +983,7 @@
"ControlNet": "ControlNet",
"Custom Nodes": "自定义节点",
"Flux": "Flux",
"SD3_5": "SD3.5",
"SDXL": "SDXL",
"Image": "图片",
"Upscaling": "放大",
"Video": "视频"
},
@@ -1000,7 +1013,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 +1028,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 +1056,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": "从模板开始"

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

View File

@@ -34,6 +34,7 @@ import { useWorkflowService } from '@/services/workflowService'
import { useCommandStore } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useExtensionStore } from '@/stores/extensionStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { KeyComboImpl, useKeybindingStore } from '@/stores/keybindingStore'
import { useModelStore } from '@/stores/modelStore'
import { SYSTEM_NODE_DEFS, useNodeDefStore } from '@/stores/nodeDefStore'
@@ -480,9 +481,8 @@ export class ComfyApp {
if (match) {
const uri = event.dataTransfer.getData(match)?.split('\n')?.[0]
if (uri) {
await this.handleFile(
new File([await (await fetch(uri)).blob()], uri)
)
const blob = await (await fetch(uri)).blob()
await this.handleFile(new File([blob], uri, { type: blob.type }))
}
}
}
@@ -1172,6 +1172,16 @@ export class ComfyApp {
const executionStore = useExecutionStore()
executionStore.lastNodeErrors = null
let comfyOrgAuthToken =
(await useFirebaseAuthStore().getIdToken()) ?? undefined
// Check if we're in a secure context before using the auth token
if (comfyOrgAuthToken && !window.isSecureContext) {
comfyOrgAuthToken = undefined
console.warn(
'Auth token not used: Not in a secure context. Authentication requires a secure connection.'
)
}
try {
while (this.#queueItems.length) {
const { number, batchCount } = this.#queueItems.pop()!
@@ -1183,7 +1193,7 @@ export class ComfyApp {
const p = await this.graphToPrompt()
try {
const res = await api.queuePrompt(number, p)
const res = await api.queuePrompt(number, p, comfyOrgAuthToken)
executionStore.lastNodeErrors = res.node_errors ?? null
if (executionStore.lastNodeErrors?.length) {
this.canvas.draw(true, true)
@@ -1509,15 +1519,16 @@ export class ComfyApp {
if (!def?.input) continue
// @ts-expect-error fixme ts strict error
for (const widget of node.widgets) {
if (widget.type === 'combo') {
if (def['input'].required?.[widget.name] !== undefined) {
// @ts-expect-error Requires discriminated union
widget.options.values = def['input'].required[widget.name][0]
} else if (def['input'].optional?.[widget.name] !== undefined) {
// @ts-expect-error Requires discriminated union
widget.options.values = def['input'].optional[widget.name][0]
if (node.widgets) {
for (const widget of node.widgets) {
if (widget.type === 'combo') {
if (def['input'].required?.[widget.name] !== undefined) {
// @ts-expect-error Requires discriminated union
widget.options.values = def['input'].required[widget.name][0]
} else if (def['input'].optional?.[widget.name] !== undefined) {
// @ts-expect-error Requires discriminated union
widget.options.values = def['input'].optional[widget.name][0]
}
}
}
}

View File

@@ -103,7 +103,6 @@ abstract class BaseDOMWidgetImpl<V extends object | string>
readonly node: LGraphNode
constructor(obj: {
id: string
node: LGraphNode
name: string
type: string
@@ -114,7 +113,7 @@ abstract class BaseDOMWidgetImpl<V extends object | string>
this.name = obj.name
this.options = obj.options
this.id = obj.id
this.id = generateUUID()
this.node = obj.node
}
@@ -172,7 +171,6 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
readonly element: T
constructor(obj: {
id: string
node: LGraphNode
name: string
type: string
@@ -235,7 +233,6 @@ export class ComponentWidgetImpl<V extends object | string>
readonly inputSpec: InputSpec
constructor(obj: {
id: string
node: LGraphNode
name: string
component: Component
@@ -293,7 +290,6 @@ LGraphNode.prototype.addDOMWidget = function <
options: DOMWidgetOptions<V> = {}
): DOMWidget<T, V> {
const widget = new DOMWidgetImpl({
id: generateUUID(),
node: this,
name,
type,

View File

@@ -92,6 +92,10 @@ export function createImageHost(node) {
}
return {
el,
getCurrentImage() {
// @ts-expect-error fixme ts strict error
return currentImgs?.[0]
},
// @ts-expect-error fixme ts strict error
updateImages(imgs) {
// @ts-expect-error fixme ts strict error

View File

@@ -19,7 +19,6 @@ import TemplateWorkflowsDialogHeader from '@/components/templates/TemplateWorkfl
import { t } from '@/i18n'
import type { ExecutionErrorWsMessage } from '@/schemas/apiSchema'
import { type ShowDialogOptions, useDialogStore } from '@/stores/dialogStore'
import { ApiNodeCost } from '@/types/apiNodeTypes'
import { ManagerTab } from '@/types/comfyManagerTypes'
export type ConfirmationDialogType =
@@ -225,14 +224,14 @@ export const useDialogService = () => {
* @returns Promise that resolves to true if user clicks login, false if cancelled
*/
async function showApiNodesSignInDialog(
apiNodes: ApiNodeCost[]
apiNodeNames: string[]
): Promise<boolean> {
return new Promise<boolean>((resolve) => {
dialogStore.showDialog({
key: 'api-nodes-signin',
component: ApiNodesSignInContent,
props: {
apiNodes,
apiNodeNames,
onLogin: () => showSignInDialog().then((result) => resolve(result)),
onCancel: () => resolve(false)
},

View File

@@ -1,6 +1,5 @@
import {
type IContextMenuValue,
type INodeInputSlot,
LGraphEventMode,
LGraphNode,
LiteGraph,
@@ -8,6 +7,7 @@ import {
type Vector2
} from '@comfyorg/litegraph'
import type {
ISerialisableNodeInput,
ISerialisableNodeOutput,
ISerialisedNode
} from '@comfyorg/litegraph/dist/types/serialisation'
@@ -41,7 +41,6 @@ import {
import { useExtensionService } from './extensionService'
const PRIMITIVE_TYPES = new Set(['INT', 'FLOAT', 'BOOLEAN', 'STRING', 'COMBO'])
export const CONFIG = Symbol()
export const GET_CONFIG = Symbol()
@@ -124,7 +123,8 @@ export const useLitegraphService = () => {
}
/**
* @internal Add a widget to the node. For primitive types, an input socket is also added.
* @internal Add a widget to the node. For both primitive types and custom widgets
* (unless `socketless`), an input socket is also added.
*/
#addInputWidget(inputSpec: InputSpec) {
const inputName = inputSpec.name
@@ -152,7 +152,7 @@ export const useLitegraphService = () => {
})
}
if (PRIMITIVE_TYPES.has(inputSpec.type)) {
if (!widget?.options?.socketless) {
const inputSpecV1 = transformInputSpecV2ToV1(inputSpec)
this.addInput(inputName, inputSpec.type, {
shape: inputSpec.isOptional ? RenderShape.HollowCircle : undefined,
@@ -222,7 +222,7 @@ export const useLitegraphService = () => {
// Note: input name is unique in a node definition, so we can lookup
// input by name.
const inputByName = new Map<string, INodeInputSlot>(
const inputByName = new Map<string, ISerialisableNodeInput>(
data.inputs?.map((input) => [input.name, input]) ?? []
)
// Inputs defined by the node definition.

View File

@@ -11,7 +11,7 @@ import { useSettingStore } from '@/stores/settingStore'
import { useToastStore } from '@/stores/toastStore'
import { ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { appendJsonExt } from '@/utils/formatUtil'
import { appendJsonExt, generateUUID } from '@/utils/formatUtil'
import { useDialogService } from './dialogService'
@@ -95,7 +95,7 @@ export const useWorkflowService = () => {
await workflowStore.saveWorkflow(workflow)
} else {
// Generate new id when saving existing workflow as a new file
const id = crypto.randomUUID()
const id = generateUUID()
const state = JSON.parse(
JSON.stringify(workflow.activeState)
) as ComfyWorkflowJSON

View File

@@ -1,10 +1,15 @@
import {
type Auth,
GithubAuthProvider,
GoogleAuthProvider,
type User,
type UserCredential,
browserLocalPersistence,
createUserWithEmailAndPassword,
onAuthStateChanged,
setPersistence,
signInWithEmailAndPassword,
signInWithPopup,
signOut
} from 'firebase/auth'
import { defineStore } from 'pinia'
@@ -18,6 +23,10 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
const currentUser = ref<User | null>(null)
const isInitialized = ref(false)
// Providers
const googleProvider = new GoogleAuthProvider()
const githubProvider = new GithubAuthProvider()
// Getters
const isAuthenticated = computed(() => !!currentUser.value)
const userEmail = computed(() => currentUser.value?.email)
@@ -26,6 +35,9 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
// Get auth from VueFire and listen for auth state changes
const auth = useFirebaseAuth()
if (auth) {
// Set persistence to localStorage (works in both browser and Electron)
void setPersistence(auth, browserLocalPersistence)
onAuthStateChanged(auth, (user) => {
currentUser.value = user
isInitialized.value = true
@@ -68,6 +80,16 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
createUserWithEmailAndPassword(authInstance, email, password)
)
const loginWithGoogle = async (): Promise<UserCredential> =>
executeAuthAction((authInstance) =>
signInWithPopup(authInstance, googleProvider)
)
const loginWithGithub = async (): Promise<UserCredential> =>
executeAuthAction((authInstance) =>
signInWithPopup(authInstance, githubProvider)
)
const logout = async (): Promise<void> =>
executeAuthAction((authInstance) => signOut(authInstance))
@@ -94,6 +116,8 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
login,
register,
logout,
getIdToken
getIdToken,
loginWithGoogle,
loginWithGithub
}
})

View File

@@ -8,10 +8,12 @@ import { isVideoNode } from '@/utils/litegraphUtil'
const createOutputs = (
filenames: string[],
type: string
type: string,
isAnimated: boolean
): ExecutedWsMessage['output'] => {
return {
images: filenames.map((image) => ({ type, ...parseFilePath(image) }))
images: filenames.map((image) => ({ type, ...parseFilePath(image) })),
animated: filenames.map((image) => isAnimated && image.endsWith('.webp'))
}
}
@@ -52,18 +54,21 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
function setNodeOutputs(
node: LGraphNode,
filenames: string | string[] | ResultItem,
{ folder = 'input' }: { folder?: string } = {}
{
folder = 'input',
isAnimated = false
}: { folder?: string; isAnimated?: boolean } = {}
) {
if (!filenames || !node) return
const nodeId = getNodeId(node)
if (typeof filenames === 'string') {
app.nodeOutputs[nodeId] = createOutputs([filenames], folder)
app.nodeOutputs[nodeId] = createOutputs([filenames], folder, isAnimated)
} else if (!Array.isArray(filenames)) {
app.nodeOutputs[nodeId] = filenames
} else {
const resultItems = createOutputs(filenames, folder)
const resultItems = createOutputs(filenames, folder, isAnimated)
if (!resultItems?.images?.length) return
app.nodeOutputs[nodeId] = resultItems
}

View File

@@ -1,13 +1,12 @@
import _ from 'lodash'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { ref } from 'vue'
import type { Settings } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import type { SettingParams } from '@/types/settingTypes'
import type { TreeNode } from '@/types/treeExplorerTypes'
import { buildTree } from '@/utils/treeUtil'
export const getSettingInfo = (setting: SettingParams) => {
const parts = setting.category || setting.id.split('.')
@@ -38,28 +37,6 @@ export const useSettingStore = defineStore('setting', () => {
const settingValues = ref<Record<string, any>>({})
const settingsById = ref<Record<string, SettingParams>>({})
const settingTree = computed<SettingTreeNode>(() => {
const root = buildTree(
Object.values(settingsById.value).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
})
/**
* Check if a setting's value exists, i.e. if the user has set it manually.
* @param key - The key of the setting to check.
@@ -150,7 +127,6 @@ export const useSettingStore = defineStore('setting', () => {
return {
settingValues,
settingsById,
settingTree,
addSetting,
loadSettingValues,
set,

View File

@@ -1,8 +1,8 @@
import { groupBy } from 'lodash'
import { defineStore } from 'pinia'
import { computed, ref, shallowRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { st } from '@/i18n'
import { api } from '@/scripts/api'
import type {
TemplateGroup,
@@ -13,7 +13,6 @@ import { normalizeI18nKey } from '@/utils/formatUtil'
export const useWorkflowTemplatesStore = defineStore(
'workflowTemplates',
() => {
const { t } = useI18n()
const customTemplates = shallowRef<{ [moduleName: string]: string[] }>({})
const coreTemplates = shallowRef<WorkflowTemplates[]>([])
const isLoaded = ref(false)
@@ -22,11 +21,9 @@ export const useWorkflowTemplatesStore = defineStore(
const allTemplates = [
...coreTemplates.value.map((template) => ({
...template,
title: t(
title: st(
`templateWorkflows.category.${normalizeI18nKey(template.title)}`,
{
defaultValue: template.title
}
template.title ?? template.moduleName
)
})),
...Object.entries(customTemplates.value).map(
@@ -46,12 +43,11 @@ export const useWorkflowTemplatesStore = defineStore(
return Object.entries(
groupBy(allTemplates, (template) =>
template.moduleName === 'default'
? t('templateWorkflows.category.ComfyUI Examples', {
defaultValue: 'ComfyUI Examples'
})
: t('templateWorkflows.category.Custom Nodes', {
defaultValue: 'Custom Nodes'
})
? st(
'templateWorkflows.category.ComfyUI Examples',
'ComfyUI Examples'
)
: st('templateWorkflows.category.Custom Nodes', 'Custom Nodes')
)
).map(([label, modules]) => ({ label, modules }))
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,9 @@
export interface TemplateInfo {
name: string
/**
* Optional title which is used as the fallback if the name is not in the locales dictionary.
*/
title?: string
tutorialUrl?: string
mediaType: string
mediaSubtype: string

View File

@@ -10,3 +10,20 @@ export const is_all_same_aspect_ratio = (imgs: HTMLImageElement[]): boolean => {
return true
}
export const fitDimensionsToNodeWidth = (
width: number,
height: number,
nodeWidth: number,
minHeight: number = 64
): { minHeight: number; minWidth: number } => {
const intrinsicAspectRatio = width / height
if (!intrinsicAspectRatio || isNaN(intrinsicAspectRatio))
return { minHeight: 0, minWidth: 0 }
// Set min. height s.t. image spans node's x-axis while maintaining aspect ratio
const minWidth = nodeWidth
const calculatedHeight = Math.max(minWidth / intrinsicAspectRatio, minHeight)
return { minHeight: calculatedHeight, minWidth }
}

View File

@@ -178,7 +178,10 @@ describe('parseComfyWorkflow', () => {
// Git hash
'080e6d4af809a46852d1c4b7ed85f06e8a3a72be',
// Special case
'unknown'
'unknown',
// Git describe
'v0.3.9-7-g1419dee',
'v0.3.9-7-g1419dee-dirty'
]
it.each(validVersionStrings)('valid version: %s', async (ver) => {
const workflow = JSON.parse(JSON.stringify(defaultGraph))

View File

@@ -13,7 +13,12 @@ vi.mock('firebase/auth', () => ({
signInWithEmailAndPassword: vi.fn(),
createUserWithEmailAndPassword: vi.fn(),
signOut: vi.fn(),
onAuthStateChanged: vi.fn()
onAuthStateChanged: vi.fn(),
signInWithPopup: vi.fn(),
GoogleAuthProvider: vi.fn(),
GithubAuthProvider: vi.fn(),
browserLocalPersistence: 'browserLocalPersistence',
setPersistence: vi.fn().mockResolvedValue(undefined)
}))
describe('useFirebaseAuthStore', () => {
@@ -61,6 +66,13 @@ describe('useFirebaseAuthStore', () => {
expect(store.error).toBe(null)
})
it('should set persistence to local storage on initialization', () => {
expect(firebaseAuth.setPersistence).toHaveBeenCalledWith(
mockAuth,
firebaseAuth.browserLocalPersistence
)
})
it('should properly clean up error state between operations', async () => {
// First, cause an error
const mockError = new Error('Invalid password')
@@ -272,4 +284,90 @@ describe('useFirebaseAuthStore', () => {
expect(tokenAfterLogout).toBeNull()
})
})
describe('social authentication', () => {
describe('loginWithGoogle', () => {
it('should sign in with Google', async () => {
const mockUserCredential = { user: mockUser }
vi.mocked(firebaseAuth.signInWithPopup).mockResolvedValue(
mockUserCredential as any
)
const result = await store.loginWithGoogle()
expect(firebaseAuth.signInWithPopup).toHaveBeenCalledWith(
mockAuth,
expect.any(firebaseAuth.GoogleAuthProvider)
)
expect(result).toEqual(mockUserCredential)
expect(store.loading).toBe(false)
expect(store.error).toBe(null)
})
it('should handle Google sign in errors', async () => {
const mockError = new Error('Google authentication failed')
vi.mocked(firebaseAuth.signInWithPopup).mockRejectedValue(mockError)
await expect(store.loginWithGoogle()).rejects.toThrow(
'Google authentication failed'
)
expect(firebaseAuth.signInWithPopup).toHaveBeenCalledWith(
mockAuth,
expect.any(firebaseAuth.GoogleAuthProvider)
)
expect(store.loading).toBe(false)
expect(store.error).toBe('Google authentication failed')
})
})
describe('loginWithGithub', () => {
it('should sign in with Github', async () => {
const mockUserCredential = { user: mockUser }
vi.mocked(firebaseAuth.signInWithPopup).mockResolvedValue(
mockUserCredential as any
)
const result = await store.loginWithGithub()
expect(firebaseAuth.signInWithPopup).toHaveBeenCalledWith(
mockAuth,
expect.any(firebaseAuth.GithubAuthProvider)
)
expect(result).toEqual(mockUserCredential)
expect(store.loading).toBe(false)
expect(store.error).toBe(null)
})
it('should handle Github sign in errors', async () => {
const mockError = new Error('Github authentication failed')
vi.mocked(firebaseAuth.signInWithPopup).mockRejectedValue(mockError)
await expect(store.loginWithGithub()).rejects.toThrow(
'Github authentication failed'
)
expect(firebaseAuth.signInWithPopup).toHaveBeenCalledWith(
mockAuth,
expect.any(firebaseAuth.GithubAuthProvider)
)
expect(store.loading).toBe(false)
expect(store.error).toBe('Github authentication failed')
})
})
it('should handle concurrent social login attempts correctly', async () => {
const mockUserCredential = { user: mockUser }
vi.mocked(firebaseAuth.signInWithPopup).mockResolvedValue(
mockUserCredential as any
)
const googleLoginPromise = store.loginWithGoogle()
const githubLoginPromise = store.loginWithGithub()
await Promise.all([googleLoginPromise, githubLoginPromise])
expect(store.loading).toBe(false)
})
})
})

View File

@@ -37,7 +37,6 @@ describe('useSettingStore', () => {
it('should initialize with empty settings', () => {
expect(store.settingValues).toEqual({})
expect(store.settingsById).toEqual({})
expect(store.settingTree.children).toEqual([])
})
describe('loadSettingValues', () => {

View File

@@ -35,6 +35,7 @@
"src/**/*.vue",
"src/types/**/*.d.ts",
"tests-ui/**/*",
"global.d.ts"
"global.d.ts",
"vite.config.mts"
]
}

View File

@@ -1,12 +1,17 @@
import vue from '@vitejs/plugin-vue'
import dotenv from 'dotenv'
import path from 'path'
import IconsResolver from 'unplugin-icons/resolver'
import Icons from 'unplugin-icons/vite'
import Components from 'unplugin-vue-components/vite'
import { Plugin, defineConfig } from 'vite'
import { defineConfig } from 'vite'
import type { UserConfigExport } from 'vitest/config'
import {
addElementVnodeExportPlugin,
comfyAPIPlugin,
generateImportMapPlugin
} from './build/plugins'
dotenv.config()
const IS_DEV = process.env.NODE_ENV === 'development'
@@ -14,86 +19,6 @@ const SHOULD_MINIFY = process.env.ENABLE_MINIFY === 'true'
// vite dev server will listen on all addresses, including LAN and public addresses
const VITE_REMOTE_DEV = process.env.VITE_REMOTE_DEV === 'true'
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 comfyAPIPlugin(): Plugin {
return {
name: 'comfy-api-plugin',
transform(code: string, id: string) {
if (IS_DEV) 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
}
}
}
}
}
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
}
const DEV_SERVER_COMFYUI_URL =
process.env.DEV_SERVER_COMFYUI_URL || 'http://127.0.0.1:8188'
@@ -110,7 +35,7 @@ export default defineConfig({
target: DEV_SERVER_COMFYUI_URL,
// Return empty array for extensions API as these modules
// are not on vite's dev server.
bypass: (req, res, options) => {
bypass: (req, res, _options) => {
if (req.url === '/api/extensions') {
res.end(JSON.stringify([]))
}
@@ -136,7 +61,12 @@ export default defineConfig({
plugins: [
vue(),
comfyAPIPlugin(),
comfyAPIPlugin(IS_DEV),
generateImportMapPlugin([
{ name: 'vue', pattern: /[\\/]node_modules[\\/]vue[\\/]/ },
{ name: 'primevue', pattern: /[\\/]node_modules[\\/]primevue[\\/]/ }
]),
addElementVnodeExportPlugin(),
Icons({
compiler: 'vue3'
@@ -184,7 +114,9 @@ export default defineConfig({
),
__SENTRY_DSN__: JSON.stringify(process.env.SENTRY_DSN || ''),
__ALGOLIA_APP_ID__: JSON.stringify(process.env.ALGOLIA_APP_ID || ''),
__ALGOLIA_API_KEY__: JSON.stringify(process.env.ALGOLIA_API_KEY || '')
__ALGOLIA_API_KEY__: JSON.stringify(process.env.ALGOLIA_API_KEY || ''),
__USE_PROD_FIREBASE_CONFIG__:
process.env.USE_PROD_FIREBASE_CONFIG === 'true'
},
resolve: {