Compare commits
54 Commits
export-gen
...
v1.17.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8f4dc95bb | ||
|
|
c1bc664edd | ||
|
|
e7fe2046ba | ||
|
|
bf4ad38e9b | ||
|
|
2b024bb186 | ||
|
|
6e5930c355 | ||
|
|
6151d487c6 | ||
|
|
e027a9bf44 | ||
|
|
53ee5904e8 | ||
|
|
f82bb71b1e | ||
|
|
40d08a890d | ||
|
|
ebf3c0c049 | ||
|
|
e77d5c1f57 | ||
|
|
b5c1da22db | ||
|
|
0006dd3855 | ||
|
|
7355209c12 | ||
|
|
2aef0a9af8 | ||
|
|
b74887d543 | ||
|
|
bf4ae227b3 | ||
|
|
184bb582da | ||
|
|
3bc3179763 | ||
|
|
eb100894ce | ||
|
|
9a992cb14d | ||
|
|
133aa9bc87 | ||
|
|
3204637e5a | ||
|
|
b2cb719026 | ||
|
|
add805460c | ||
|
|
9621b8f339 | ||
|
|
6be381b15d | ||
|
|
8afe99f48c | ||
|
|
9cd11261f9 | ||
|
|
fbc6665ff4 | ||
|
|
2daa51421c | ||
|
|
0f175c3dc1 | ||
|
|
8d4263c94e | ||
|
|
04580ac031 | ||
|
|
cd35f1d86d | ||
|
|
5d584577fe | ||
|
|
10a96d1af6 | ||
|
|
03392a3cc7 | ||
|
|
12576243ad | ||
|
|
e2a6dc2ec8 | ||
|
|
2f77d74891 | ||
|
|
dacb59f5d3 | ||
|
|
74f991ec1b | ||
|
|
6bc03a624e | ||
|
|
1fb015e046 | ||
|
|
87bf2310b6 | ||
|
|
f1a25989d7 | ||
|
|
236e3fb3e9 | ||
|
|
50382827bc | ||
|
|
41675805b6 | ||
|
|
6321fae6f3 | ||
|
|
06caa21a4d |
2
.github/workflows/release.yaml
vendored
@@ -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
|
||||
|
||||
3
.github/workflows/test-ui.yaml
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
`
|
||||
|
||||
BIN
browser_tests/assets/animated_webp.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
11
browser_tests/assets/widgets/load_animated_webp.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"8": {
|
||||
"inputs": {
|
||||
"image": "animated_web.webp"
|
||||
},
|
||||
"class_type": "DevToolsLoadAnimatedImageTest",
|
||||
"_meta": {
|
||||
"title": "Load Animated Image"
|
||||
}
|
||||
}
|
||||
}
|
||||
60
browser_tests/assets/widgets/save_animated_webp.json
Normal 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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
After Width: | Height: | Size: 141 KiB |
@@ -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'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 113 KiB |
59
build/plugins/addElementVnodeExportPlugin.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
82
build/plugins/comfyAPIPlugin.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
103
build/plugins/generateImportMapPlugin.ts
Normal 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
@@ -0,0 +1,3 @@
|
||||
export { addElementVnodeExportPlugin } from './addElementVnodeExportPlugin'
|
||||
export { comfyAPIPlugin } from './comfyAPIPlugin'
|
||||
export { generateImportMapPlugin } from './generateImportMapPlugin'
|
||||
1
global.d.ts
vendored
@@ -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
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.17.0",
|
||||
"version": "1.17.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.17.0",
|
||||
"version": "1.17.6",
|
||||
"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": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.17.0",
|
||||
"version": "1.17.6",
|
||||
"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",
|
||||
|
||||
@@ -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')
|
||||
31
src/components/common/ApiNodesList.vue
Normal 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>
|
||||
@@ -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
|
||||
}>()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
158
src/components/dialog/content/setting/CreditsPanel.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<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')"
|
||||
:loading
|
||||
@click="handlePurchaseCreditsClick"
|
||||
/>
|
||||
</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'
|
||||
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { usdToMicros } from '@/utils/formatUtil'
|
||||
|
||||
// TODO: Mock data - in a real implementation, this would come from a store or API
|
||||
const creditBalance = ref(0.05)
|
||||
|
||||
// TODO: Either: (1) Get checkout URL that allows setting price on Stripe side, (2) Add number selection on credits panel
|
||||
const selectedCurrencyAmount = usdToMicros(10)
|
||||
|
||||
const selectedCurrency = 'usd' // For now, only USD is supported on comfy-api backend
|
||||
|
||||
interface CreditHistoryItemData {
|
||||
title: string
|
||||
timestamp: string
|
||||
amount: number
|
||||
isPositive: boolean
|
||||
}
|
||||
|
||||
const { initiateCreditPurchase, loading } = useFirebaseAuthStore()
|
||||
|
||||
const handlePurchaseCreditsClick = async () => {
|
||||
const response = await initiateCreditPurchase({
|
||||
amount_micros: selectedCurrencyAmount,
|
||||
currency: selectedCurrency
|
||||
})
|
||||
if (!response) return
|
||||
|
||||
const { checkout_url } = response
|
||||
if (checkout_url !== undefined) {
|
||||
// Go to Stripe checkout page
|
||||
window.open(checkout_url, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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, ' '))
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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> =>
|
||||
|
||||
@@ -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,
|
||||
|
||||
122
src/composables/setting/useSettingSearch.ts
Normal 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
|
||||
}
|
||||
}
|
||||
141
src/composables/setting/useSettingUI.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -616,6 +616,15 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
function: () => {
|
||||
dialogService.showManagerProgressDialog()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.User.OpenSignInDialog',
|
||||
icon: 'pi pi-user',
|
||||
label: 'Open Sign In Dialog',
|
||||
versionAdded: '1.17.6',
|
||||
function: async () => {
|
||||
await dialogService.showSignInDialog()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
],
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"Comfy-Desktop_CheckForUpdates": {
|
||||
"label": "Check for Updates"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "Open Custom Nodes Folder"
|
||||
},
|
||||
@@ -170,6 +173,9 @@
|
||||
"Comfy_Undo": {
|
||||
"label": "Undo"
|
||||
},
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "Open Sign In Dialog"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "Close Current Workflow"
|
||||
},
|
||||
|
||||
@@ -110,7 +110,8 @@
|
||||
"migrate": "Migrate",
|
||||
"updateAvailable": "Update Available",
|
||||
"login": "Login",
|
||||
"learnMore": "Learn more"
|
||||
"learnMore": "Learn more",
|
||||
"amount": "Amount"
|
||||
},
|
||||
"manager": {
|
||||
"title": "Custom Nodes Manager",
|
||||
@@ -463,8 +464,7 @@
|
||||
"ControlNet": "ControlNet",
|
||||
"Upscaling": "Upscaling",
|
||||
"Video": "Video",
|
||||
"SD3_5": "SD3.5",
|
||||
"SDXL": "SDXL",
|
||||
"Image": "Image",
|
||||
"Area Composition": "Area Composition",
|
||||
"3D": "3D",
|
||||
"Audio": "Audio"
|
||||
@@ -510,20 +510,23 @@
|
||||
"ltxv_image_to_video": "LTXV Image to Video",
|
||||
"ltxv_text_to_video": "LTXV Text to Video",
|
||||
"mochi_text_to_video_example": "Mochi Text to Video",
|
||||
"hunyuan_video_text_to_video": "Hunyuan Video Text to Video"
|
||||
"hunyuan_video_text_to_video": "Hunyuan Video Text to Video",
|
||||
"wan2_1_fun_inp": "Wan 2.1 Inpainting",
|
||||
"wan2_1_fun_control": "Wan 2.1 ControlNet"
|
||||
},
|
||||
"SD3_5": {
|
||||
"Image": {
|
||||
"sd3_5_simple_example": "SD3.5 Simple",
|
||||
"sd3_5_large_canny_controlnet_example": "SD3.5 Large Canny ControlNet",
|
||||
"sd3_5_large_depth": "SD3.5 Large Depth",
|
||||
"sd3_5_large_blur": "SD3.5 Large Blur"
|
||||
},
|
||||
"SDXL": {
|
||||
"sd3_5_large_blur": "SD3.5 Large Blur",
|
||||
"sdxl_simple_example": "SDXL Simple",
|
||||
"sdxl_refiner_prompt_example": "SDXL Refiner Prompt",
|
||||
"sdxl_revision_text_prompts": "SDXL Revision Text Prompts",
|
||||
"sdxl_revision_zero_positive": "SDXL Revision Zero Positive",
|
||||
"sdxlturbo_example": "SDXL Turbo"
|
||||
"sdxlturbo_example": "SDXL Turbo",
|
||||
"hidream_i1_dev": "HiDream I1 Dev",
|
||||
"hidream_i1_fast": "HiDream I1 Fast",
|
||||
"hidream_i1_full": "HiDream I1 Full"
|
||||
},
|
||||
"Area Composition": {
|
||||
"area_composition": "Area Composition",
|
||||
@@ -600,6 +603,7 @@
|
||||
"Workflow": "Workflow",
|
||||
"Edit": "Edit",
|
||||
"Help": "Help",
|
||||
"Check for Updates": "Check for Updates",
|
||||
"Open Custom Nodes Folder": "Open Custom Nodes Folder",
|
||||
"Open Inputs Folder": "Open Inputs Folder",
|
||||
"Open Logs Folder": "Open Logs Folder",
|
||||
@@ -657,6 +661,7 @@
|
||||
"Show Settings Dialog": "Show Settings Dialog",
|
||||
"Toggle Theme (Dark/Light)": "Toggle Theme (Dark/Light)",
|
||||
"Undo": "Undo",
|
||||
"Open Sign In Dialog": "Open Sign In Dialog",
|
||||
"Close Current Workflow": "Close Current Workflow",
|
||||
"Next Opened Workflow": "Next Opened Workflow",
|
||||
"Previous Opened Workflow": "Previous Opened Workflow",
|
||||
@@ -722,7 +727,8 @@
|
||||
"Load 3D": "Load 3D",
|
||||
"Camera": "Camera",
|
||||
"Scene": "Scene",
|
||||
"3D": "3D"
|
||||
"3D": "3D",
|
||||
"Light": "Light"
|
||||
},
|
||||
"serverConfigItems": {
|
||||
"listen": {
|
||||
@@ -990,7 +996,9 @@
|
||||
"desktopUpdate": {
|
||||
"title": "Updating ComfyUI Desktop",
|
||||
"description": "ComfyUI Desktop is installing new dependencies. This may take a few minutes.",
|
||||
"terminalDefaultMessage": "Any console output from the update will be shown here."
|
||||
"terminalDefaultMessage": "Any console output from the update will be shown here.",
|
||||
"updateFoundTitle": "Update Found (v{version})",
|
||||
"updateAvailableMessage": "An update is available. Do you want to restart and update now?"
|
||||
},
|
||||
"clipboard": {
|
||||
"successMessage": "Copied to clipboard",
|
||||
@@ -1102,5 +1110,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"
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"Comfy-Desktop_CheckForUpdates": {
|
||||
"label": "Buscar actualizaciones"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "Abrir carpeta de nodos personalizados"
|
||||
},
|
||||
@@ -170,6 +173,9 @@
|
||||
"Comfy_Undo": {
|
||||
"label": "Deshacer"
|
||||
},
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "Abrir diálogo de inicio de sesión"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "Cerrar Flujo de Trabajo Actual"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
@@ -600,6 +613,7 @@
|
||||
"Open Logs Folder": "Abrir carpeta de registros",
|
||||
"Open Models Folder": "Abrir carpeta de modelos",
|
||||
"Open Outputs Folder": "Abrir carpeta de salidas",
|
||||
"Open Sign In Dialog": "Abrir diálogo de inicio de sesión",
|
||||
"Open extra_model_paths_yaml": "Abrir extra_model_paths.yaml",
|
||||
"Pin/Unpin Selected Items": "Anclar/Desanclar elementos seleccionados",
|
||||
"Pin/Unpin Selected Nodes": "Anclar/Desanclar nodos seleccionados",
|
||||
@@ -877,6 +891,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 +984,7 @@
|
||||
"ControlNet": "ControlNet",
|
||||
"Custom Nodes": "Nodos Personalizados",
|
||||
"Flux": "Flux",
|
||||
"SD3_5": "SD3.5",
|
||||
"SDXL": "SDXL",
|
||||
"Image": "Imagen",
|
||||
"Upscaling": "Ampliación",
|
||||
"Video": "Video"
|
||||
},
|
||||
@@ -1015,13 +1029,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 +1057,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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"Comfy-Desktop_CheckForUpdates": {
|
||||
"label": "Vérifier les mises à jour"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "Ouvrir le dossier des nœuds personnalisés"
|
||||
},
|
||||
@@ -170,6 +173,9 @@
|
||||
"Comfy_Undo": {
|
||||
"label": "Annuler"
|
||||
},
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "Ouvrir la boîte de dialogue de connexion"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "Fermer le flux de travail actuel"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
@@ -600,6 +613,7 @@
|
||||
"Open Logs Folder": "Ouvrir le dossier des journaux",
|
||||
"Open Models Folder": "Ouvrir le dossier des modèles",
|
||||
"Open Outputs Folder": "Ouvrir le dossier des sorties",
|
||||
"Open Sign In Dialog": "Ouvrir la boîte de dialogue de connexion",
|
||||
"Open extra_model_paths_yaml": "Ouvrir extra_model_paths.yaml",
|
||||
"Pin/Unpin Selected Items": "Épingler/Désépingler les éléments sélectionnés",
|
||||
"Pin/Unpin Selected Nodes": "Épingler/Désépingler les nœuds sélectionnés",
|
||||
@@ -877,6 +891,7 @@
|
||||
"Graph": "Graphique",
|
||||
"Group": "Groupe",
|
||||
"Keybinding": "Raccourci Clavier",
|
||||
"Light": "Clair",
|
||||
"Link": "Lien",
|
||||
"LinkRelease": "Libération de Lien",
|
||||
"LiteGraph": "Lite Graph",
|
||||
@@ -969,8 +984,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 +1029,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 +1057,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"
|
||||
|
||||
@@ -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 l’intensité avec laquelle les lumières illuminent les objets lors de la création d’un 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 l’intensité lumineuse dans les scènes 3D. Cela fixe la limite supérieure de luminosité pouvant être réglée lors de l’ajustement de l’éclairage dans tout widget 3D."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMinimum": {
|
||||
"name": "Intensité lumineuse minimale",
|
||||
"tooltip": "Définit la valeur minimale autorisée de l’intensité lumineuse pour les scènes 3D. Cela définit la limite inférieure de luminosité pouvant être réglée lors de l’ajustement de l’éclairage dans tout widget 3D."
|
||||
},
|
||||
"Comfy_Load3D_ShowGrid": {
|
||||
"name": "Afficher la Grille",
|
||||
"tooltip": "Basculer pour afficher la grille par défaut"
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"Comfy-Desktop_CheckForUpdates": {
|
||||
"label": "更新を確認する"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "カスタムノードフォルダを開く"
|
||||
},
|
||||
@@ -170,6 +173,9 @@
|
||||
"Comfy_Undo": {
|
||||
"label": "元に戻す"
|
||||
},
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "サインインダイアログを開く"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "現在のワークフローを閉じる"
|
||||
},
|
||||
|
||||
@@ -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": "クリップスペース",
|
||||
@@ -600,6 +613,7 @@
|
||||
"Open Logs Folder": "ログフォルダを開く",
|
||||
"Open Models Folder": "モデルフォルダを開く",
|
||||
"Open Outputs Folder": "出力フォルダを開く",
|
||||
"Open Sign In Dialog": "サインインダイアログを開く",
|
||||
"Open extra_model_paths_yaml": "extra_model_paths.yamlを開く",
|
||||
"Pin/Unpin Selected Items": "選択したアイテムのピン留め/ピン留め解除",
|
||||
"Pin/Unpin Selected Nodes": "選択したノードのピン留め/ピン留め解除",
|
||||
@@ -877,6 +891,7 @@
|
||||
"Graph": "グラフ",
|
||||
"Group": "グループ",
|
||||
"Keybinding": "キー割り当て",
|
||||
"Light": "ライト",
|
||||
"Link": "リンク",
|
||||
"LinkRelease": "リンク解除",
|
||||
"LiteGraph": "Lite Graph",
|
||||
@@ -969,8 +984,7 @@
|
||||
"ControlNet": "ControlNet",
|
||||
"Custom Nodes": "カスタムノード",
|
||||
"Flux": "Flux",
|
||||
"SD3_5": "SD3.5",
|
||||
"SDXL": "SDXL",
|
||||
"Image": "画像",
|
||||
"Upscaling": "アップスケーリング",
|
||||
"Video": "ビデオ"
|
||||
},
|
||||
@@ -1015,13 +1029,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 +1057,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": "テンプレートを利用して開始"
|
||||
|
||||
@@ -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": "デフォルトでグリッドを表示するには切り替えます"
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"Comfy-Desktop_CheckForUpdates": {
|
||||
"label": "업데이트 확인"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "사용자 정의 노드 폴더 열기"
|
||||
},
|
||||
@@ -170,6 +173,9 @@
|
||||
"Comfy_Undo": {
|
||||
"label": "실행 취소"
|
||||
},
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "로그인 대화상자 열기"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "현재 워크플로우 닫기"
|
||||
},
|
||||
|
||||
@@ -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": "클립스페이스",
|
||||
@@ -600,6 +613,7 @@
|
||||
"Open Logs Folder": "로그 폴더 열기",
|
||||
"Open Models Folder": "모델 폴더 열기",
|
||||
"Open Outputs Folder": "출력 폴더 열기",
|
||||
"Open Sign In Dialog": "로그인 대화 상자 열기",
|
||||
"Open extra_model_paths_yaml": "extra_model_paths.yaml 열기",
|
||||
"Pin/Unpin Selected Items": "선택한 항목 고정/고정 해제",
|
||||
"Pin/Unpin Selected Nodes": "선택한 노드 고정/고정 해제",
|
||||
@@ -877,6 +891,7 @@
|
||||
"Graph": "그래프",
|
||||
"Group": "그룹",
|
||||
"Keybinding": "키 바인딩",
|
||||
"Light": "라이트",
|
||||
"Link": "링크",
|
||||
"LinkRelease": "링크 해제",
|
||||
"LiteGraph": "LiteGraph",
|
||||
@@ -969,8 +984,7 @@
|
||||
"ControlNet": "컨트롤넷",
|
||||
"Custom Nodes": "사용자 정의 노드",
|
||||
"Flux": "FLUX",
|
||||
"SD3_5": "SD3.5",
|
||||
"SDXL": "SDXL",
|
||||
"Image": "이미지",
|
||||
"Upscaling": "업스케일링",
|
||||
"Video": "비디오"
|
||||
},
|
||||
@@ -1015,13 +1029,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 +1057,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": "템플릿으로 시작하기"
|
||||
|
||||
@@ -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": "기본적으로 그리드를 표시하도록 전환"
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"Comfy-Desktop_CheckForUpdates": {
|
||||
"label": "Проверить наличие обновлений"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "Открыть папку пользовательских нод"
|
||||
},
|
||||
@@ -170,6 +173,9 @@
|
||||
"Comfy_Undo": {
|
||||
"label": "Отменить"
|
||||
},
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "Открыть окно входа"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "Закрыть текущий рабочий процесс"
|
||||
},
|
||||
|
||||
@@ -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": "Клиппространство",
|
||||
@@ -600,6 +613,7 @@
|
||||
"Open Logs Folder": "Открыть папку журналов",
|
||||
"Open Models Folder": "Открыть папку моделей",
|
||||
"Open Outputs Folder": "Открыть папку выходных данных",
|
||||
"Open Sign In Dialog": "Открыть окно входа",
|
||||
"Open extra_model_paths_yaml": "Открыть extra_model_paths.yaml",
|
||||
"Pin/Unpin Selected Items": "Закрепить/открепить выбранные элементы",
|
||||
"Pin/Unpin Selected Nodes": "Закрепить/открепить выбранные ноды",
|
||||
@@ -877,6 +891,7 @@
|
||||
"Graph": "Граф",
|
||||
"Group": "Группа",
|
||||
"Keybinding": "Сочетание клавиш",
|
||||
"Light": "Светлый",
|
||||
"Link": "Ссылка",
|
||||
"LinkRelease": "Освобождение ссылки",
|
||||
"LiteGraph": "Lite Graph",
|
||||
@@ -969,8 +984,7 @@
|
||||
"ControlNet": "ControlNet",
|
||||
"Custom Nodes": "Пользовательские узлы",
|
||||
"Flux": "Flux",
|
||||
"SD3_5": "SD3.5",
|
||||
"SDXL": "SDXL",
|
||||
"Image": "Изображение",
|
||||
"Upscaling": "Увеличение разрешения",
|
||||
"Video": "Видео"
|
||||
},
|
||||
@@ -1015,17 +1029,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 +1057,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": "Начните с шаблона"
|
||||
|
||||
@@ -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": "Переключиться, чтобы показывать сетку по умолчанию"
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"Comfy-Desktop_CheckForUpdates": {
|
||||
"label": "检查更新"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "打开自定义节点文件夹"
|
||||
},
|
||||
@@ -170,6 +173,9 @@
|
||||
"Comfy_Undo": {
|
||||
"label": "撤销"
|
||||
},
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "打开登录对话框"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "关闭当前工作流"
|
||||
},
|
||||
|
||||
@@ -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": "剪贴空间",
|
||||
@@ -600,6 +613,7 @@
|
||||
"Open Logs Folder": "打开日志文件夹",
|
||||
"Open Models Folder": "打开模型文件夹",
|
||||
"Open Outputs Folder": "打开输出文件夹",
|
||||
"Open Sign In Dialog": "打开登录对话框",
|
||||
"Open extra_model_paths_yaml": "打开 extra_model_paths.yaml",
|
||||
"Pin/Unpin Selected Items": "固定/取消固定选定项目",
|
||||
"Pin/Unpin Selected Nodes": "固定/取消固定选定节点",
|
||||
@@ -877,6 +891,7 @@
|
||||
"Graph": "画面",
|
||||
"Group": "组节点",
|
||||
"Keybinding": "快捷键",
|
||||
"Light": "浅色",
|
||||
"Link": "连线",
|
||||
"LinkRelease": "释放链接",
|
||||
"LiteGraph": "画面",
|
||||
@@ -969,8 +984,7 @@
|
||||
"ControlNet": "ControlNet",
|
||||
"Custom Nodes": "自定义节点",
|
||||
"Flux": "Flux",
|
||||
"SD3_5": "SD3.5",
|
||||
"SDXL": "SDXL",
|
||||
"Image": "图片",
|
||||
"Upscaling": "放大",
|
||||
"Video": "视频"
|
||||
},
|
||||
@@ -1000,7 +1014,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 +1029,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 +1057,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": "从模板开始"
|
||||
|
||||
@@ -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": "默认显示网格开关"
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
import {
|
||||
type Auth,
|
||||
GithubAuthProvider,
|
||||
GoogleAuthProvider,
|
||||
type User,
|
||||
type UserCredential,
|
||||
browserLocalPersistence,
|
||||
createUserWithEmailAndPassword,
|
||||
onAuthStateChanged,
|
||||
setPersistence,
|
||||
signInWithEmailAndPassword,
|
||||
signInWithPopup,
|
||||
signOut
|
||||
} from 'firebase/auth'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useFirebaseAuth } from 'vuefire'
|
||||
|
||||
import { operations } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type CreditPurchaseResponse =
|
||||
operations['InitiateCreditPurchase']['responses']['201']['content']['application/json']
|
||||
type CreditPurchasePayload =
|
||||
operations['InitiateCreditPurchase']['requestBody']['content']['application/json']
|
||||
|
||||
// TODO: Switch to prod api based on environment (requires prod api to be ready)
|
||||
const API_BASE_URL = 'https://stagingapi.comfy.org'
|
||||
|
||||
export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
// State
|
||||
const loading = ref(false)
|
||||
@@ -18,6 +33,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 +45,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 +90,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))
|
||||
|
||||
@@ -78,6 +110,39 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
return null
|
||||
}
|
||||
|
||||
const addCredits = async (
|
||||
requestBodyContent: CreditPurchasePayload
|
||||
): Promise<CreditPurchaseResponse | null> => {
|
||||
const token = await getIdToken()
|
||||
if (!token) {
|
||||
error.value = 'Cannot add credits: User not authenticated'
|
||||
return null
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/customers/credit`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(requestBodyContent)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
error.value = `Failed to initiate credit purchase: ${errorData.message}`
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO: start polling /listBalance until balance is updated or n retries fail or report no change
|
||||
return response.json()
|
||||
}
|
||||
|
||||
const initiateCreditPurchase = async (
|
||||
requestBodyContent: CreditPurchasePayload
|
||||
): Promise<CreditPurchaseResponse | null> =>
|
||||
executeAuthAction((_) => addCredits(requestBodyContent))
|
||||
|
||||
return {
|
||||
// State
|
||||
loading,
|
||||
@@ -94,6 +159,9 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
getIdToken
|
||||
getIdToken,
|
||||
loginWithGoogle,
|
||||
loginWithGithub,
|
||||
initiateCreditPurchase
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }))
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -415,3 +415,17 @@ export function compareVersions(
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a USD amount to microdollars (1/1,000,000 of a dollar).
|
||||
* This conversion is commonly used in financial systems to avoid floating-point precision issues
|
||||
* by representing monetary values as integers.
|
||||
*
|
||||
* @param usd - The amount in US dollars to convert
|
||||
* @returns The amount in microdollars (multiplied by 1,000,000)
|
||||
* @example
|
||||
* usdToMicros(1.23) // returns 1230000
|
||||
*/
|
||||
export function usdToMicros(usd: number): number {
|
||||
return Math.round(usd * 1_000_000)
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"src/**/*.vue",
|
||||
"src/types/**/*.d.ts",
|
||||
"tests-ui/**/*",
|
||||
"global.d.ts"
|
||||
"global.d.ts",
|
||||
"vite.config.mts"
|
||||
]
|
||||
}
|
||||
|
||||