Compare commits

...

89 Commits

Author SHA1 Message Date
Comfy Org PR Bot
fcbdee54ec 1.18.0 (#3552)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
Co-authored-by: Chenlei Hu <huchenlei@proton.me>
2025-04-24 12:43:44 -04:00
filtered
a944372f39 Improve default node sizes (#3596)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-24 12:42:16 -04:00
Comfy Org PR Bot
df51e89311 [chore] Update litegraph to 0.14.0 (#3602)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-04-25 01:00:15 +10:00
Terry Jia
b314435f81 add importmap for vue-i18n (#3595) 2025-04-23 22:34:19 -04:00
Chenlei Hu
ba367c0214 [nit] Rename credit history to invoice history (#3598)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-23 22:33:52 -04:00
Chenlei Hu
64ad6a9bb0 Re-style TopUpCreditDialog to match design (#3597) 2025-04-23 22:28:35 -04:00
AustinMroz
3819db5ec4 Introduce widgetType option to inputSpec (#3550) 2025-04-23 20:36:01 -04:00
filtered
80517e8204 [Test] Add test expectation (#3593) 2025-04-24 08:48:06 +10:00
Comfy Org PR Bot
40034e77f9 1.17.11 (#3592)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-04-24 08:04:24 +10:00
filtered
2ef8b7cfd7 Fix opening search from context menu (#3591)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-24 07:54:27 +10:00
Comfy Org PR Bot
9cf3a0e568 1.17.10 (#3590)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-23 15:19:22 -04:00
Chenlei Hu
f562cf27cd [API Node] Workaround custom node hijack on api.queuePrompt (#3589)
Co-authored-by: filtered <176114999+webfiltered@users.noreply.github.com>
2025-04-23 15:18:37 -04:00
Chenlei Hu
4244a0a258 [Cleanup] Remove unused dialog functions (#3588) 2025-04-23 11:46:50 -04:00
Comfy Org PR Bot
ad3d2fe2e9 1.17.9 (#3585)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-23 11:16:46 -04:00
Chenlei Hu
4c23cfbd4d [API Node] Better execution error handling (#3587)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-23 11:15:38 -04:00
Chenlei Hu
9e10e55633 [Schema] Add api_node flag to node def schema (#3586) 2025-04-23 10:47:08 -04:00
Christian Byrne
59cbe90fd3 [API Node] Reset password (#3578)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-23 10:13:17 -04:00
Christian Byrne
16bd9abccd Translate user and credits settings categories (#3580)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-23 10:12:14 -04:00
Christian Byrne
e84bdc96cf Update FAQ link (#3579) 2025-04-23 10:12:02 -04:00
Comfy Org PR Bot
a57be36d4d 1.17.8 (#3573)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-22 22:26:48 -04:00
Christian Byrne
3bd508c001 Use generalized prod config var (#3574) 2025-04-22 22:26:31 -04:00
Chenlei Hu
612500a4dc [nit] Fix typo (#3577) 2025-04-22 21:35:26 -04:00
Christian Byrne
e9723407d8 [API Node] Fix credits fetch condition (#3575) 2025-04-22 21:33:44 -04:00
Christian Byrne
a01aa39423 [API Node] Show error toast (#3576) 2025-04-22 21:32:52 -04:00
Chenlei Hu
ab94a55858 Show user avatar on top menu if available (#3572) 2025-04-22 19:47:34 -04:00
Christian Byrne
1bcf5e28d4 [API Node] Contact support button (#3571)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-22 19:47:23 -04:00
Chenlei Hu
9e247063aa [nit] Adjust font size of User Panel header (#3570) 2025-04-22 19:03:40 -04:00
Comfy Org PR Bot
cdddf359a8 1.17.7 (#3569)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-22 18:52:13 -04:00
Christian Byrne
8558f87547 [API Node] User management (#3567)
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Chenlei Hu <hcl@comfy.org>
2025-04-22 18:48:45 -04:00
Chenlei Hu
262991db6b [Bug] Prevent node pasting in signin dialog (#3568) 2025-04-22 16:18:25 -04:00
Chenlei Hu
585d52e24e [Bug] Fix DOM widgets position when canvas bounding box change (#3565)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-22 15:11:54 -04:00
Chenlei Hu
b7535755f0 Revert "[nit] Remove tab index on canvas element" (#3566) 2025-04-22 12:58:37 -04:00
Chenlei Hu
6b7b0f6ec1 [nit] Remove tab index on canvas element (#3563) 2025-04-22 11:40:22 -04:00
filtered
c7318bcf0a Update node search to use litegraph LinkConnector (#3546)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-22 10:35:49 -04:00
Christian Byrne
11f909436c [Manager] Fix reactivity of node pack version options dropdown (#3557) 2025-04-22 09:45:46 -04:00
Comfy Org PR Bot
d8f4dc95bb 1.17.6 (#3558)
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-04-21 22:50:43 -07:00
Christian Byrne
c1bc664edd Add command to open signin dialog (#3556)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-21 22:46:31 -07:00
Christian Byrne
e7fe2046ba Checkout on credit add (#3555) 2025-04-21 22:46:05 -07:00
Comfy Org PR Bot
bf4ad38e9b 1.17.5 (#3551)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-21 20:08:30 -04:00
Chenlei Hu
2b024bb186 [Cleanup] Remove LGraphNode.isValidWidgetLink (#3549) 2025-04-21 15:48:55 -04:00
filtered
6e5930c355 [API] Add sockets to custom widgets by default (#3548) 2025-04-21 15:24:48 -04:00
Comfy Org PR Bot
6151d487c6 [chore] Update litegraph to 0.13.8 (#3547)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-04-22 02:17:12 +10:00
Comfy Org PR Bot
e027a9bf44 [chore] Update litegraph to 0.13.7 (#3544)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-04-21 11:00:10 -04:00
filtered
53ee5904e8 [TS] Fix serialisation type (#3541) 2025-04-21 21:13:42 +10:00
Christian Byrne
f82bb71b1e Fix client => canvas position conversion (#3540) 2025-04-21 20:21:25 +10:00
Terry Jia
40d08a890d [3d] move default values of backgroundColor, LightIntensity, LightMaximum, LightMinimum, LightStep to settings panel (#3536)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-20 23:08:23 -04:00
Chenlei Hu
ebf3c0c049 [API Nodes] Add credit management panel UI (#3535)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-20 22:11:43 -04:00
Comfy Org PR Bot
e77d5c1f57 1.17.4 (#3533)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-20 21:04:14 -04:00
Chenlei Hu
b5c1da22db [API Nodes] Remove cost from signin required dialog (#3532) 2025-04-20 17:02:42 -04:00
Chenlei Hu
0006dd3855 [Refactor] Split custom vite plugins to files under build/plugins (#3531) 2025-04-20 14:07:45 -04:00
Terry Jia
7355209c12 build vue and primevue separately and generate importmap (#3473)
Co-authored-by: Chenlei Hu <hcl@comfy.org>
2025-04-20 13:43:22 -04:00
Chenlei Hu
2aef0a9af8 [Bug] Fix model name (#3530) 2025-04-20 13:16:13 -04:00
Chenlei Hu
b74887d543 [i18n] Use gpt-o4-mini for translation (#3529) 2025-04-20 13:13:00 -04:00
Benjamin Lu
bf4ae227b3 Add manual update check (#3504)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Chenlei Hu <huchenlei@proton.me>
2025-04-20 12:28:59 -04:00
Christian Byrne
184bb582da [Manager] Check if node is core node when inferring node pack (#3521) 2025-04-20 12:13:50 -04:00
Christian Byrne
3bc3179763 Apply filename text replacements (e.g., %date:hh:mm:ss%) in all save nodes (#3523) 2025-04-20 12:12:28 -04:00
Comfy Org PR Bot
eb100894ce [chore] Update Comfy Registry API types from comfy-api@7bc8051 (#3524)
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-04-20 12:12:14 -04:00
Comfy Org PR Bot
9a992cb14d [chore] Update litegraph to 0.13.6 (#3528)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-04-21 01:18:27 +10:00
Comfy Org PR Bot
133aa9bc87 [chore] Update litegraph to 0.13.5 (#3527)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-04-20 21:31:02 +10:00
Dr.Lt.Data
3204637e5a refine locales/ko (#3526) 2025-04-20 21:14:33 +10:00
Comfy Org PR Bot
b2cb719026 1.17.3 (#3520)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-19 22:21:18 -04:00
Christian Byrne
add805460c Fix drag and drop image with embedded workflow on Firefox (#3519)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-19 22:19:48 -04:00
Benjamin Lu
9621b8f339 [Desktop] Support Nvidia Blackwell (#3480)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-04-19 21:42:30 -04:00
Christian Byrne
6be381b15d Allow git describe formatted versions of node packs in workflows (#3518) 2025-04-19 21:42:01 -04:00
Christian Byrne
8afe99f48c Fix node.widgets undefined on refresh (#3515) 2025-04-20 10:21:31 +10:00
Christian Byrne
9cd11261f9 [API Node] Set auth persistence in local stoarge (#3514) 2025-04-18 23:15:07 -04:00
Comfy Org PR Bot
fbc6665ff4 1.17.2 (#3513)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-18 22:26:07 -04:00
Christian Byrne
2daa51421c Remove fetch-templates script (#3500) 2025-04-18 20:34:03 -04:00
Christian Byrne
0f175c3dc1 [Api Node] Add ToS and privacy policy links (#3511) 2025-04-18 20:21:20 -04:00
Christian Byrne
8d4263c94e Use dev firebase and switch to prod in release workflow (#3499) 2025-04-18 17:23:10 -04:00
Chenlei Hu
04580ac031 [SettingUI] Group setting menu items (#3510) 2025-04-18 16:47:32 -04:00
Chenlei Hu
cd35f1d86d [Refactor] Generate DOM widget id in constructor (#3508) 2025-04-18 13:47:16 -04:00
Chenlei Hu
5d584577fe [Bug] Fix uuid generation in insecure context (#3505) 2025-04-18 11:43:25 -04:00
filtered
10a96d1af6 [TMP] Temporarily disable hidream template test (#3502) 2025-04-18 21:34:02 +10:00
Comfy Org PR Bot
03392a3cc7 1.17.1 (#3486)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-17 22:57:57 -04:00
Christian Byrne
12576243ad Remove unused tailwind classes (#3495) 2025-04-17 22:57:47 -04:00
Christian Byrne
e2a6dc2ec8 [Templates] Use fallbacks when translating template titles and category names (#3494) 2025-04-17 22:57:03 -04:00
Chenlei Hu
2f77d74891 [Refactor] Move tree logic to setting composable (#3491) 2025-04-17 16:34:42 -04:00
Chenlei Hu
dacb59f5d3 [Refactor] Extract setting dialog logic into composables (#3490) 2025-04-17 15:55:16 -04:00
Christian Byrne
74f991ec1b Translate Wan2.1-Fun template titles (#3489)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-17 15:55:08 -04:00
Christian Byrne
6bc03a624e Add HiDream templates translations (#3485)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-17 13:46:11 -04:00
Christian Byrne
1fb015e046 Remove templates build from release process (#3481) 2025-04-17 10:41:36 -04:00
Christian Byrne
87bf2310b6 Support previewing animated image uploads (#3479)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-17 10:20:09 -04:00
filtered
f1a25989d7 Fix corrupt workflow permanently corrupts session (#3484) 2025-04-17 10:18:14 -04:00
filtered
236e3fb3e9 Log errors in generic error handler (#3482) 2025-04-17 15:16:05 +10:00
Chenlei Hu
50382827bc [API Nodes] Add auth_token_comfy_org to queue prompt request body (#3477) 2025-04-16 15:26:25 -04:00
Christian Byrne
41675805b6 [Test] Add LGraphBadge light theme browser test (#3475)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-16 15:10:14 -04:00
Makki Shizu
6321fae6f3 fix 2_pass_pose_worship zh translation (#3474) 2025-04-16 11:04:28 -04:00
Chenlei Hu
06caa21a4d [API Nodes] Setup Google/Github login (#3471) 2025-04-15 20:56:18 -04:00
138 changed files with 8314 additions and 1153 deletions

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -251,4 +251,17 @@ test.describe('Release context menu', () => {
'link-release-context-menu.png'
)
})
test('Can search and add node from context menu', async ({
comfyPage,
comfyMouse
}) => {
await comfyPage.disconnectEdge()
await comfyMouse.move({ x: 10, y: 10 })
await comfyPage.clickContextMenuItem('Search')
await comfyPage.searchBox.fillAndSelectFirstNode('CLIP Prompt')
await expect(comfyPage.canvas).toHaveScreenshot(
'link-context-menu-search.png'
)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View File

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

View File

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

View File

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

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

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

1
global.d.ts vendored
View File

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

20
package-lock.json generated
View File

@@ -1,18 +1,18 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.17.0",
"version": "1.18.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@comfyorg/comfyui-frontend",
"version": "1.17.0",
"version": "1.18.0",
"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.14.0",
"@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.14.0",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.14.0.tgz",
"integrity": "sha512-hYXGiqO8FbhhqXjYSCKD4k7WRvOWaFJBvuSiWYH0KvgAyg1upA3gaMbsAVfdD1ZW3U0Zt0j1jwjCUpDxdBT6QQ==",
"license": "MIT"
},
"node_modules/@cspotcode/source-map-support": {

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.17.0",
"version": "1.18.0",
"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.14.0",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",

View File

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

View File

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

View File

@@ -9,10 +9,10 @@
{{ 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 />
<Button :label="t('g.learnMore')" link @click="handleLearnMoreClick" />
<div class="flex gap-2">
<Button
:label="t('g.cancel')"
@@ -30,14 +30,15 @@
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
}>()
const handleLearnMoreClick = () => {
window.open('https://www.comfy.org/faq', '_blank')
}
</script>

View File

@@ -25,6 +25,13 @@
:label="$t('issueReport.helpFix')"
@click="showSendReport"
/>
<Button
v-if="authStore.currentUser"
v-show="!reportOpen"
text
:label="$t('issueReport.contactSupportTitle')"
@click="showContactSupport"
/>
</div>
<template v-if="reportOpen">
<Divider />
@@ -72,6 +79,8 @@ import FindIssueButton from '@/components/dialog/content/error/FindIssueButton.v
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import type { ReportField } from '@/types/issueReportTypes'
import {
@@ -81,6 +90,8 @@ import {
import ReportIssuePanel from './error/ReportIssuePanel.vue'
const authStore = useFirebaseAuthStore()
const { error } = defineProps<{
error: Omit<ErrorReportData, 'workflow' | 'systemStats' | 'serverLogs'> & {
/**
@@ -123,6 +134,10 @@ const stackTraceField = computed<ReportField>(() => {
}
})
const showContactSupport = async () => {
await useCommandStore().execute('Comfy.ContactSupport')
}
onMounted(async () => {
if (!systemStatsStore.systemStats) {
await systemStatsStore.fetchSystemStats()

View File

@@ -10,15 +10,21 @@
/>
<Listbox
v-model="activeCategory"
:options="categories"
:options="groupedMenuTreeNodes"
option-label="translatedLabel"
option-group-label="label"
option-group-children="children"
scroll-height="100%"
:option-disabled="
(option: SettingTreeNode) =>
!queryIsEmpty && !searchResultsCategories.has(option.label ?? '')
"
class="border-none w-full"
/>
>
<template #optiongroup>
<Divider class="my-0" />
</template>
</Listbox>
</ScrollPanel>
<Divider layout="vertical" class="mx-1 2xl:mx-4 hidden md:flex" />
<Divider layout="horizontal" class="flex md:hidden" />
@@ -42,6 +48,8 @@
</PanelTemplate>
<AboutPanel />
<UserPanel />
<CreditsPanel />
<Suspense>
<KeybindingPanel />
<template #fallback>
@@ -73,30 +81,33 @@ 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 { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { SettingTreeNode } from '@/stores/settingStore'
import { ISettingGroup, SettingParams } from '@/types/settingTypes'
import { isElectron } from '@/utils/envUtil'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { flattenTree } from '@/utils/treeUtil'
import AboutPanel from './setting/AboutPanel.vue'
import ColorPaletteMessage from './setting/ColorPaletteMessage.vue'
import CreditsPanel from './setting/CreditsPanel.vue'
import CurrentUserMessage from './setting/CurrentUserMessage.vue'
import FirstTimeUIMessage from './setting/FirstTimeUIMessage.vue'
import PanelTemplate from './setting/PanelTemplate.vue'
import SettingsPanel from './setting/SettingsPanel.vue'
import UserPanel from './setting/UserPanel.vue'
const props = defineProps<{
defaultPanel?: 'about' | 'keybinding' | 'extension' | 'server-config'
const { defaultPanel } = defineProps<{
defaultPanel?:
| 'about'
| 'keybinding'
| 'extension'
| 'server-config'
| 'user'
| 'credits'
}>()
const KeybindingPanel = defineAsyncComponent(
@@ -109,71 +120,25 @@ 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 {
searchQuery,
searchResultsCategories,
queryIsEmpty,
inSearch,
handleSearch: handleSearchBase,
getSearchResults
} = useSettingSearch()
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 authStore = useFirebaseAuthStore()
// Sort groups for a category
const sortedGroups = (category: SettingTreeNode): ISettingGroup[] => {
return [...(category.children ?? [])]
.sort((a, b) => a.label.localeCompare(b.label))
@@ -183,98 +148,29 @@ 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) => {
if (!tabValue.value) {
activeCategory.value = oldValue
}
if (activeCategory.value?.key === 'credits') {
void authStore.fetchBalance()
}
})
</script>
@@ -313,14 +209,8 @@ watch(activeCategory, (_, oldValue) => {
}
}
/* Show a separator line above the Keybinding tab */
/* This indicates the start of custom setting panels */
.settings-sidebar :deep(.p-listbox-option[aria-label='Keybinding']) {
position: relative;
}
.settings-sidebar :deep(.p-listbox-option[aria-label='Keybinding'])::before {
@apply content-[''] top-0 left-0 absolute w-full;
border-top: 1px solid var(--p-divider-border-color);
/* Hide the first group separator */
.settings-sidebar :deep(.p-listbox-option-group:nth-child(1)) {
display: none;
}
</style>

View File

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

View File

@@ -0,0 +1,161 @@
<template>
<div class="flex flex-col w-96 p-2 gap-10">
<div v-if="isInsufficientCredits" class="flex flex-col gap-4">
<h1 class="text-2xl font-medium leading-normal my-0">
{{ $t('credits.topUp.insufficientTitle') }}
</h1>
<p class="text-base my-0">
{{ $t('credits.topUp.insufficientMessage') }}
</p>
</div>
<!-- Balance Section -->
<div class="flex justify-between items-center">
<div class="flex flex-col gap-2 w-full">
<div class="text-muted text-base">
{{ $t('credits.yourCreditBalance') }}
</div>
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-2">
<Tag
severity="secondary"
icon="pi pi-dollar"
rounded
class="text-amber-400 p-1"
/>
<span class="text-2xl">{{ formattedBalance }}</span>
</div>
<Button
outlined
severity="secondary"
:label="$t('credits.topUp.seeDetails')"
icon="pi pi-arrow-up-right"
@click="handleSeeDetails"
/>
</div>
</div>
</div>
<!-- Amount Input Section -->
<div class="flex flex-col gap-2">
<span class="text-muted text-sm"
>{{ $t('credits.topUp.quickPurchase') }}:</span
>
<div class="grid grid-cols-[2fr_1fr] gap-2">
<template v-for="amount in amountOptions" :key="amount">
<div class="flex items-center gap-2">
<Tag
severity="secondary"
icon="pi pi-dollar"
rounded
class="text-amber-400 p-1"
/>
<span class="text-xl">{{ amount }}</span>
</div>
<Button
:severity="
preselectedAmountOption === amount ? 'primary' : 'secondary'
"
:outlined="preselectedAmountOption !== amount"
:label="$t('credits.topUp.buyNow')"
@click="handleBuyNow(amount)"
/>
</template>
<div class="flex items-center gap-2">
<Tag
severity="secondary"
icon="pi pi-dollar"
rounded
class="text-amber-400 p-1"
/>
<InputNumber
v-model="customAmount"
:min="1"
:max="1000"
:step="1"
show-buttons
:allow-empty="false"
:highlight-on-focus="true"
pt:pc-input-text:root="w-24"
@blur="
(e: InputNumberBlurEvent) => (customAmount = Number(e.value))
"
@input="
(e: InputNumberInputEvent) => (customAmount = Number(e.value))
"
/>
</div>
<ProgressSpinner v-if="loading" class="w-8 h-8" />
<Button
v-else
:label="$t('credits.topUp.buyNow')"
severity="secondary"
outlined
@click="handleBuyNow(customAmount)"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import InputNumber, {
type InputNumberBlurEvent,
type InputNumberInputEvent
} from 'primevue/inputnumber'
import ProgressSpinner from 'primevue/progressspinner'
import Tag from 'primevue/tag'
import { computed, onBeforeUnmount, ref } from 'vue'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { formatMetronomeCurrency, usdToMicros } from '@/utils/formatUtil'
const {
isInsufficientCredits = false,
amountOptions = [5, 10, 20, 50],
preselectedAmountOption = 10
} = defineProps<{
isInsufficientCredits?: boolean
amountOptions?: number[]
preselectedAmountOption?: number
}>()
const authStore = useFirebaseAuthStore()
const customAmount = ref<number>(100)
const didClickBuyNow = ref(false)
const loading = computed(() => authStore.loading)
const formattedBalance = computed(() => {
if (!authStore.balance) return '0.000'
return formatMetronomeCurrency(authStore.balance.amount_micros, 'usd')
})
const handleSeeDetails = async () => {
const response = await authStore.accessBillingPortal()
if (!response?.billing_portal_url) return
window.open(response.billing_portal_url, '_blank')
}
const handleBuyNow = async (amount: number) => {
const response = await authStore.initiateCreditPurchase({
amount_micros: usdToMicros(amount),
currency: 'usd'
})
if (!response?.checkout_url) return
didClickBuyNow.value = true
// Go to Stripe checkout page
window.open(response.checkout_url, '_blank')
}
onBeforeUnmount(() => {
if (didClickBuyNow.value) {
// If clicked buy now, then returned back to the dialog and closed, fetch the balance
void authStore.fetchBalance()
}
})
</script>

View File

@@ -23,75 +23,136 @@
</div>
</template>
<div class="p-4 mt-2 border border-round surface-border shadow-1">
<div class="flex flex-row gap-3 mb-2">
<div v-for="field in fields" :key="field.value">
<FormField
v-if="field.optIn"
v-slot="$field"
:name="field.value"
class="flex space-x-1"
<div class="flex flex-col gap-6">
<FormField
v-slot="$field"
name="contactInfo"
:initial-value="authStore.currentUser?.email"
>
<div class="self-stretch inline-flex justify-start items-center">
<label for="contactInfo" class="pb-2 pt-0 opacity-80">{{
$t('issueReport.email')
}}</label>
</div>
<InputText
id="contactInfo"
v-bind="$field"
class="w-full"
:placeholder="$t('issueReport.provideEmail')"
/>
<Message
v-if="$field?.error && $field.touched && $field.value !== ''"
severity="error"
size="small"
variant="simple"
>
<Checkbox
{{ t('issueReport.validation.invalidEmail') }}
</Message>
</FormField>
<FormField v-slot="$field" name="helpType">
<div class="flex flex-col gap-2">
<div
class="self-stretch inline-flex justify-start items-center gap-2.5"
>
<label for="helpType" class="pb-2 pt-0 opacity-80">{{
$t('issueReport.whatDoYouNeedHelpWith')
}}</label>
</div>
<Dropdown
v-bind="$field"
v-model="selection"
:input-id="field.value"
:value="field.value"
v-model="$field.value"
:options="helpTypes"
option-label="label"
option-value="value"
:placeholder="$t('issueReport.selectIssue')"
class="w-full"
/>
<label :for="field.value">{{ field.label }}</label>
<Message
v-if="$field?.error"
severity="error"
size="small"
variant="simple"
>
{{ t('issueReport.validation.selectIssueType') }}
</Message>
</div>
</FormField>
<div class="flex flex-col gap-2">
<div
class="self-stretch inline-flex justify-start items-center gap-2.5"
>
<span class="pb-2 pt-0 opacity-80">{{
$t('issueReport.whatCanWeInclude')
}}</span>
</div>
<div class="flex flex-row gap-3">
<div v-for="field in fields" :key="field.value">
<FormField
v-if="field.optIn"
v-slot="$field"
:name="field.value"
class="flex space-x-1"
>
<Checkbox
v-bind="$field"
v-model="selection"
:input-id="field.value"
:value="field.value"
/>
<label :for="field.value">{{ field.label }}</label>
</FormField>
</div>
</div>
</div>
<div class="flex flex-col gap-2">
<FormField v-slot="$field" name="details">
<div
class="self-stretch inline-flex justify-start items-center gap-2.5"
>
<label for="details" class="pb-2 pt-0 opacity-80">{{
$t('issueReport.describeTheProblem')
}}</label>
</div>
<Textarea
v-bind="$field"
id="details"
class="w-full"
rows="5"
:placeholder="$t('issueReport.provideAdditionalDetails')"
:aria-label="$t('issueReport.provideAdditionalDetails')"
/>
<Message
v-if="$field?.error && $field.touched && $field.value"
severity="error"
size="small"
variant="simple"
>
{{ t('issueReport.validation.maxLength') }}
</Message>
</FormField>
</div>
</div>
<FormField v-slot="$field" class="mb-4" name="details">
<Textarea
v-bind="$field"
class="w-full"
rows="5"
:placeholder="$t('issueReport.provideAdditionalDetails')"
:aria-label="$t('issueReport.provideAdditionalDetails')"
/>
<Message
v-if="$field?.error && $field.touched && $field.value"
severity="error"
size="small"
variant="simple"
>
{{ t('issueReport.validation.maxLength') }}
</Message>
</FormField>
<FormField v-slot="$field" name="contactInfo">
<InputText
v-bind="$field"
class="w-full"
:placeholder="$t('issueReport.provideEmail')"
/>
<Message
v-if="$field?.error && $field.touched && $field.value !== ''"
severity="error"
size="small"
variant="simple"
>
{{ t('issueReport.validation.invalidEmail') }}
</Message>
</FormField>
<div class="flex flex-row gap-3 mt-2">
<div v-for="checkbox in contactCheckboxes" :key="checkbox.value">
<FormField
v-slot="$field"
:name="checkbox.value"
class="flex space-x-1"
>
<Checkbox
v-bind="$field"
v-model="contactPrefs"
:input-id="checkbox.value"
:value="checkbox.value"
:disabled="
$form.contactInfo?.error || !$form.contactInfo?.value
"
/>
<label :for="checkbox.value">{{ checkbox.label }}</label>
</FormField>
<div class="flex flex-col gap-3 mt-2">
<div v-for="checkbox in contactCheckboxes" :key="checkbox.value">
<FormField
v-slot="$field"
:name="checkbox.value"
class="flex space-x-1"
>
<Checkbox
v-bind="$field"
v-model="contactPrefs"
:input-id="checkbox.value"
:value="checkbox.value"
:disabled="
$form.contactInfo?.error || !$form.contactInfo?.value
"
/>
<label :for="checkbox.value">{{ checkbox.label }}</label>
</FormField>
</div>
</div>
</div>
</div>
@@ -108,6 +169,7 @@ import _ from 'lodash'
import cloneDeep from 'lodash/cloneDeep'
import Button from 'primevue/button'
import Checkbox from 'primevue/checkbox'
import Dropdown from 'primevue/dropdown'
import InputText from 'primevue/inputtext'
import Message from 'primevue/message'
import Panel from 'primevue/panel'
@@ -122,14 +184,16 @@ import {
} from '@/schemas/issueReportSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import type {
DefaultField,
IssueReportPanelProps,
ReportField
} from '@/types/issueReportTypes'
import { isElectron } from '@/utils/envUtil'
import { generateUUID } from '@/utils/formatUtil'
const ISSUE_NAME = 'User reported issue'
const DEFAULT_ISSUE_NAME = 'User reported issue'
const props = defineProps<IssueReportPanelProps>()
const { defaultFields = ['Workflow', 'Logs', 'SystemStats', 'Settings'] } =
@@ -137,6 +201,7 @@ const { defaultFields = ['Workflow', 'Logs', 'SystemStats', 'Settings'] } =
const { t } = useI18n()
const toast = useToast()
const authStore = useFirebaseAuthStore()
const selection = ref<string[]>([])
const contactPrefs = ref<string[]>([])
@@ -147,6 +212,20 @@ const contactCheckboxes = [
{ label: t('issueReport.notifyResolve'), value: 'notifyOnResolution' }
]
const helpTypes = [
{
label: t('issueReport.helpTypes.billingPayments'),
value: 'billingPayments'
},
{
label: t('issueReport.helpTypes.loginAccessIssues'),
value: 'loginAccessIssues'
},
{ label: t('issueReport.helpTypes.giveFeedback'), value: 'giveFeedback' },
{ label: t('issueReport.helpTypes.bugReport'), value: 'bugReport' },
{ label: t('issueReport.helpTypes.somethingElse'), value: 'somethingElse' }
]
const defaultFieldsConfig: ReportField[] = [
{
label: t('issueReport.systemStats'),
@@ -213,6 +292,7 @@ const createCaptureContext = async (
level: 'error',
tags: {
errorType: props.errorType,
helpType: formData.helpType,
followUp: formData.contactInfo ? formData.followUp : false,
notifyOnResolution: formData.contactInfo
? formData.notifyOnResolution
@@ -227,11 +307,24 @@ const createCaptureContext = async (
}
}
const generateUniqueTicketId = (type: string) => `${type}-${generateUUID()}`
const submit = async (event: FormSubmitEvent) => {
if (event.valid) {
try {
const captureContext = await createCaptureContext(event.values)
captureMessage(ISSUE_NAME, captureContext)
// If it's billing or access issue, generate unique id to be used by customer service ticketing
const isValidContactInfo = event.values.contactInfo?.length
const isCustomerServiceIssue =
isValidContactInfo &&
['billingPayments', 'loginAccessIssues'].includes(
event.values.helpType || ''
)
const issueName = isCustomerServiceIssue
? `ticket-${generateUniqueTicketId(event.values.helpType || '')}`
: DEFAULT_ISSUE_NAME
captureMessage(issueName, captureContext)
submitted.value = true
toast.add({
severity: 'success',

View File

@@ -1,5 +1,6 @@
import { Form } from '@primevue/forms'
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import Checkbox from 'primevue/checkbox'
import PrimeVue from 'primevue/config'
import InputText from 'primevue/inputtext'
@@ -65,7 +66,12 @@ vi.mock('@/scripts/api', () => ({
api: {
getLogs: vi.fn().mockResolvedValue('mock logs'),
getSystemStats: vi.fn().mockResolvedValue('mock stats'),
getSettings: vi.fn().mockResolvedValue('mock settings')
getSettings: vi.fn().mockResolvedValue('mock settings'),
fetchApi: vi.fn().mockResolvedValue({
json: vi.fn().mockResolvedValue({}),
text: vi.fn().mockResolvedValue('')
}),
apiURL: vi.fn().mockReturnValue('https://test.com')
}
}))
@@ -139,12 +145,14 @@ vi.mock('@primevue/forms', () => ({
describe('ReportIssuePanel', () => {
beforeEach(() => {
vi.clearAllMocks()
const pinia = createPinia()
setActivePinia(pinia)
})
const mountComponent = (props: IssueReportPanelProps, options = {}): any => {
return mount(ReportIssuePanel, {
global: {
plugins: [PrimeVue, i18n],
plugins: [PrimeVue, i18n, createPinia()],
directives: { tooltip: Tooltip }
},
props,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,187 @@
<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 v-if="balanceLoading" class="flex items-center gap-1">
<div class="flex items-center gap-2">
<Skeleton shape="circle" width="1.5rem" height="1.5rem" />
</div>
<div class="flex-1"></div>
<Skeleton width="8rem" height="2rem" />
</div>
<div v-else class="flex items-center gap-1">
<Tag
severity="secondary"
icon="pi pi-dollar"
rounded
class="text-amber-400 p-1"
/>
<div class="text-3xl font-bold">{{ formattedBalance }}</div>
</div>
<Skeleton v-if="loading" width="2rem" height="2rem" />
<Button
v-else
:label="$t('credits.purchaseCredits')"
:loading="loading"
@click="handlePurchaseCreditsClick"
/>
</div>
<div class="flex flex-row items-center">
<Skeleton
v-if="balanceLoading"
width="12rem"
height="1rem"
class="text-xs"
/>
<div v-else-if="formattedLastUpdateTime" class="text-xs text-muted">
{{ $t('credits.lastUpdated') }}: {{ formattedLastUpdateTime }}
</div>
<Button
icon="pi pi-refresh"
text
size="small"
severity="secondary"
@click="() => authStore.fetchBalance()"
/>
</div>
</div>
<div class="flex justify-between items-center mt-8">
<Button
:label="$t('credits.invoiceHistory')"
text
severity="secondary"
icon="pi pi-arrow-up-right"
:loading="loading"
@click="handleCreditsHistoryClick"
/>
</div>
<template v-if="creditHistory.length > 0">
<div class="flex-grow">
<DataTable :value="creditHistory" :show-headers="false">
<Column field="title" :header="$t('g.name')">
<template #body="{ data }">
<div class="text-sm font-medium">{{ data.title }}</div>
<div class="text-xs text-muted">{{ data.timestamp }}</div>
</template>
</Column>
<Column field="amount" :header="$t('g.amount')">
<template #body="{ data }">
<div
:class="[
'text-base font-medium text-center',
data.isPositive ? 'text-sky-500' : 'text-red-400'
]"
>
{{ data.isPositive ? '+' : '-' }}${{
formatMetronomeCurrency(data.amount, 'usd')
}}
</div>
</template>
</Column>
</DataTable>
</div>
</template>
<Divider />
<div class="flex flex-row gap-2">
<Button
:label="$t('credits.faqs')"
text
severity="secondary"
icon="pi pi-question-circle"
@click="handleFaqClick"
/>
<Button
:label="$t('credits.messageSupport')"
text
severity="secondary"
icon="pi pi-comments"
@click="handleMessageSupport"
/>
</div>
</div>
</TabPanel>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Column from 'primevue/column'
import DataTable from 'primevue/datatable'
import Divider from 'primevue/divider'
import Skeleton from 'primevue/skeleton'
import TabPanel from 'primevue/tabpanel'
import Tag from 'primevue/tag'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useDialogService } from '@/services/dialogService'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { formatMetronomeCurrency } from '@/utils/formatUtil'
interface CreditHistoryItemData {
title: string
timestamp: string
amount: number
isPositive: boolean
}
const { t } = useI18n()
const dialogService = useDialogService()
const authStore = useFirebaseAuthStore()
const loading = computed(() => authStore.loading)
const balanceLoading = computed(() => authStore.isFetchingBalance)
const formattedBalance = computed(() => {
if (!authStore.balance) return '0.00'
return formatMetronomeCurrency(authStore.balance.amount_micros, 'usd')
})
const formattedLastUpdateTime = computed(() =>
authStore.lastBalanceUpdateTime
? authStore.lastBalanceUpdateTime.toLocaleString()
: ''
)
const handlePurchaseCreditsClick = () => {
dialogService.showTopUpCreditsDialog()
}
const handleCreditsHistoryClick = async () => {
const response = await authStore.accessBillingPortal()
if (!response) return
const { billing_portal_url } = response
if (billing_portal_url) {
window.open(billing_portal_url, '_blank')
}
}
const handleMessageSupport = () => {
dialogService.showIssueReportDialog({
title: t('issueReport.contactSupportTitle'),
subtitle: t('issueReport.contactSupportDescription'),
panelProps: {
errorType: 'BillingSupport',
defaultFields: ['Workflow', 'Logs', 'SystemStats', 'Settings']
}
})
}
const handleFaqClick = () => {
window.open('https://www.comfy.org/faq', '_blank')
}
const creditHistory = ref<CreditHistoryItemData[]>([])
</script>

View File

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

View File

@@ -36,7 +36,10 @@
>
{{ t('auth.login.passwordLabel') }}
</label>
<span class="text-muted text-base font-medium cursor-pointer">
<span
class="text-muted text-base font-medium cursor-pointer"
@click="handleForgotPassword($form.email?.value)"
>
{{ t('auth.login.forgotPassword') }}
</span>
</div>
@@ -57,7 +60,9 @@
</div>
<!-- Submit Button -->
<ProgressSpinner v-if="loading" class="w-8 h-8" />
<Button
v-else
type="submit"
:label="t('auth.login.loginButton')"
class="h-10 font-medium mt-4"
@@ -71,11 +76,19 @@ import { zodResolver } from '@primevue/forms/resolvers/zod'
import Button from 'primevue/button'
import InputText from 'primevue/inputtext'
import Password from 'primevue/password'
import ProgressSpinner from 'primevue/progressspinner'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { type SignInData, signInSchema } from '@/schemas/signInSchema'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { useToastStore } from '@/stores/toastStore'
const authStore = useFirebaseAuthStore()
const loading = computed(() => authStore.loading)
const { t } = useI18n()
const toast = useToastStore()
const emit = defineEmits<{
submit: [values: SignInData]
@@ -86,4 +99,22 @@ const onSubmit = (event: FormSubmitEvent) => {
emit('submit', event.values as SignInData)
}
}
const handleForgotPassword = async (email: string) => {
if (!email) return
await authStore.sendPasswordReset(email)
if (authStore.error) {
toast.add({
severity: 'error',
summary: t('auth.login.forgotPasswordError'),
detail: authStore.error
})
} else {
toast.add({
severity: 'success',
summary: t('auth.login.passwordResetSent'),
detail: t('auth.login.passwordResetSentDetail')
})
}
}
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,49 @@
<!-- A button that shows current authenticated user's avatar -->
<template>
<Button
v-if="isAuthenticated"
v-tooltip="{ value: $t('userSettings.title'), showDelay: 300 }"
class="user-profile-button p-1"
severity="secondary"
text
:aria-label="$t('userSettings.title')"
@click="openUserSettings"
>
<div
class="flex items-center gap-2 pr-2 rounded-full bg-[var(--p-content-background)]"
>
<!-- User Avatar if available -->
<div v-if="user?.photoURL" class="flex items-center gap-1">
<img
:src="user.photoURL"
:alt="user.displayName || ''"
class="w-8 h-8 rounded-full"
/>
</div>
<!-- User Icon if no avatar -->
<div v-else class="w-8 h-8 rounded-full flex items-center justify-center">
<i class="pi pi-user text-sm" />
</div>
<i class="pi pi-chevron-down" :style="{ fontSize: '0.5rem' }" />
</div>
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { computed } from 'vue'
import { useDialogService } from '@/services/dialogService'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
const authStore = useFirebaseAuthStore()
const dialogService = useDialogService()
const isAuthenticated = computed(() => authStore.isAuthenticated)
const user = computed(() => authStore.currentUser)
const openUserSettings = () => {
dialogService.showSettingsDialog('user')
}
</script>

View File

@@ -12,6 +12,7 @@
</div>
<div ref="menuRight" class="comfyui-menu-right flex-shrink-0" />
<Actionbar />
<CurrentUserButton class="flex-shrink-0" />
<BottomPanelToggleButton class="flex-shrink-0" />
<Button
v-tooltip="{ value: $t('menu.hideMenu'), showDelay: 300 }"
@@ -44,6 +45,7 @@ import { computed, onMounted, provide, ref } from 'vue'
import Actionbar from '@/components/actionbar/ComfyActionbar.vue'
import BottomPanelToggleButton from '@/components/topbar/BottomPanelToggleButton.vue'
import CommandMenubar from '@/components/topbar/CommandMenubar.vue'
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
import { app } from '@/scripts/app'
import { useSettingStore } from '@/stores/settingStore'
@@ -57,6 +59,7 @@ import {
const workspaceState = useWorkspaceStore()
const settingStore = useSettingStore()
const workflowTabsPosition = computed(() =>
settingStore.get('Comfy.Workflow.WorkflowTabsPosition')
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -579,6 +579,22 @@ export function useCoreCommands(): ComfyCommand[] {
})
}
},
{
id: 'Comfy.ContactSupport',
icon: 'pi pi-question',
label: 'Contact Support',
versionAdded: '1.17.8',
function: () => {
dialogService.showIssueReportDialog({
title: t('issueReport.contactSupportTitle'),
subtitle: t('issueReport.contactSupportDescription'),
panelProps: {
errorType: 'ContactSupport',
defaultFields: ['Workflow', 'Logs', 'SystemStats', 'Settings']
}
})
}
},
{
id: 'Comfy.Help.OpenComfyUIForum',
icon: 'pi pi-comments',
@@ -616,6 +632,15 @@ export function useCoreCommands(): ComfyCommand[] {
function: () => {
dialogService.showManagerProgressDialog()
}
},
{
id: 'Comfy.User.OpenSignInDialog',
icon: 'pi pi-user',
label: 'Open Sign In Dialog',
versionAdded: '1.17.6',
function: async () => {
await dialogService.showSignInDialog()
}
}
]

View File

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

View File

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

View File

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

View File

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

3
src/config/comfyApi.ts Normal file
View File

@@ -0,0 +1,3 @@
export const COMFY_API_BASE_URL = __USE_PROD_CONFIG__
? 'https://api.comfy.org'
: 'https://stagingapi.comfy.org'

View File

@@ -1,6 +1,17 @@
import { FirebaseOptions } from 'firebase/app'
export const FIREBASE_CONFIG: FirebaseOptions = {
const DEV_CONFIG: FirebaseOptions = {
apiKey: 'AIzaSyDa_YMeyzV0SkVe92vBZ1tVikWBmOU5KVE',
authDomain: 'dreamboothy-dev.firebaseapp.com',
databaseURL: 'https://dreamboothy-dev-default-rtdb.firebaseio.com',
projectId: 'dreamboothy-dev',
storageBucket: 'dreamboothy-dev.appspot.com',
messagingSenderId: '313257147182',
appId: '1:313257147182:web:be38f6ebf74345fc7618bf',
measurementId: 'G-YEVSMYXSPY'
}
const PROD_CONFIG: FirebaseOptions = {
apiKey: 'AIzaSyC2-fomLqgCjb7ELwta1I9cEarPK8ziTGs',
authDomain: 'dreamboothy.firebaseapp.com',
databaseURL: 'https://dreamboothy-default-rtdb.firebaseio.com',
@@ -10,3 +21,8 @@ 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_CONFIG=true in .env
export const FIREBASE_CONFIG: FirebaseOptions = __USE_PROD_CONFIG__
? PROD_CONFIG
: DEV_CONFIG

View File

@@ -23,5 +23,8 @@ export const CORE_MENU_COMMANDS = [
'Comfy.Help.OpenComfyUIForum'
]
],
[['Help'], ['Comfy.Help.AboutComfyUI', 'Comfy.Feedback']]
[
['Help'],
['Comfy.Help.AboutComfyUI', 'Comfy.Feedback', 'Comfy.ContactSupport']
]
]

View File

@@ -794,5 +794,14 @@ export const CORE_SETTINGS: SettingParams[] = [
type: 'boolean',
defaultValue: true,
versionAdded: '1.16.1'
},
{
id: 'LiteGraph.Node.DefaultPadding',
name: 'Always shrink new nodes',
tooltip:
'Resize nodes to the smallest possible size when created. When disabled, a newly added node will be widened slightly to show widget values.',
type: 'boolean',
defaultValue: false,
versionAdded: '1.18.0'
}
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,7 @@
{
"Comfy-Desktop_CheckForUpdates": {
"label": "Check for Updates"
},
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
"label": "Open Custom Nodes Folder"
},
@@ -80,6 +83,9 @@
"Comfy_ClearWorkflow": {
"label": "Clear Workflow"
},
"Comfy_ContactSupport": {
"label": "Contact Support"
},
"Comfy_DuplicateWorkflow": {
"label": "Duplicate Current Workflow"
},
@@ -170,6 +176,9 @@
"Comfy_Undo": {
"label": "Undo"
},
"Comfy_User_OpenSignInDialog": {
"label": "Open Sign In Dialog"
},
"Workspace_CloseWorkflow": {
"label": "Close Current Workflow"
},

View File

@@ -110,7 +110,8 @@
"migrate": "Migrate",
"updateAvailable": "Update Available",
"login": "Login",
"learnMore": "Learn more"
"learnMore": "Learn more",
"amount": "Amount"
},
"manager": {
"title": "Custom Nodes Manager",
@@ -180,9 +181,24 @@
"helpFix": "Help Fix This",
"rating": "Rating",
"feedbackTitle": "Help us improve ComfyUI by providing feedback",
"contactSupportTitle": "Contact Support",
"contactSupportDescription": "Please fill in the form below with your report",
"selectIssue": "Select the issue",
"whatDoYouNeedHelpWith": "What do you need help with?",
"whatCanWeInclude": "Specify what to include in the report",
"describeTheProblem": "Describe the problem",
"email": "Email",
"helpTypes": {
"billingPayments": "Billing / Payments",
"loginAccessIssues": "Login / Access Issues",
"giveFeedback": "Give Feedback",
"bugReport": "Bug Report",
"somethingElse": "Something Else"
},
"validation": {
"maxLength": "Message too long",
"invalidEmail": "Please enter a valid email address"
"invalidEmail": "Please enter a valid email address",
"selectIssueType": "Please select an issue type"
}
},
"color": {
@@ -463,8 +479,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 +525,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 +618,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",
@@ -627,6 +646,7 @@
"Zoom Out": "Zoom Out",
"Clear Pending Tasks": "Clear Pending Tasks",
"Clear Workflow": "Clear Workflow",
"Contact Support": "Contact Support",
"Duplicate Current Workflow": "Duplicate Current Workflow",
"Export": "Export",
"Export (API)": "Export (API)",
@@ -657,6 +677,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 +743,10 @@
"Load 3D": "Load 3D",
"Camera": "Camera",
"Scene": "Scene",
"3D": "3D"
"3D": "3D",
"Light": "Light",
"User": "User",
"Credits": "Credits"
},
"serverConfigItems": {
"listen": {
@@ -990,7 +1014,9 @@
"desktopUpdate": {
"title": "Updating ComfyUI Desktop",
"description": "ComfyUI Desktop is installing new dependencies. This may take a few minutes.",
"terminalDefaultMessage": "Any console output from the update will be shown here."
"terminalDefaultMessage": "Any console output from the update will be shown here.",
"updateFoundTitle": "Update Found (v{version})",
"updateAvailableMessage": "An update is available. Do you want to restart and update now?"
},
"clipboard": {
"successMessage": "Copied to clipboard",
@@ -1055,6 +1081,10 @@
"auth": {
"login": {
"title": "Log in to your account",
"signInOrSignUp": "Sign In / Sign Up",
"forgotPasswordError": "Failed to send password reset email",
"passwordResetSent": "Password reset email sent",
"passwordResetSentDetail": "Please check your email for a link to reset your password.",
"newUser": "New here?",
"signUp": "Sign up",
"emailLabel": "Email",
@@ -1073,7 +1103,8 @@
"andText": "and",
"privacyLink": "Privacy Policy",
"success": "Login successful",
"failed": "Login failed"
"failed": "Login failed",
"genericErrorMessage": "Sorry, we've encountered an error. Please contact {supportEmail}."
},
"signup": {
"title": "Create an account",
@@ -1086,6 +1117,11 @@
"signIn": "Sign in",
"signUpWithGoogle": "Sign up with Google",
"signUpWithGithub": "Sign up with Github"
},
"signOut": {
"signOut": "Log Out",
"success": "Signed out successfully",
"successDetail": "You have been signed out of your account."
}
},
"validation": {
@@ -1102,5 +1138,33 @@
"special": "Must contain at least one special character",
"match": "Passwords must match"
}
},
"credits": {
"credits": "Credits",
"yourCreditBalance": "Your credit balance",
"purchaseCredits": "Purchase Credits",
"invoiceHistory": "Invoice History",
"faqs": "FAQs",
"messageSupport": "Message Support",
"lastUpdated": "Last updated",
"topUp": {
"insufficientTitle": "Insufficient Credits",
"insufficientMessage": "You don't have enough credits to run this workflow.",
"quickPurchase": "Quick Purchase",
"maxAmount": "(Max. $1,000 USD)",
"buyNow": "Buy now",
"seeDetails": "See details"
}
},
"userSettings": {
"title": "User Settings",
"name": "Name",
"email": "Email",
"notSet": "Not set",
"provider": "Sign in method",
"providers": {
"google": "Google",
"github": "GitHub"
}
}
}

View File

@@ -108,6 +108,10 @@
"Hidden": "Hidden"
}
},
"Comfy_Load3D_BackgroundColor": {
"name": "Initial Background Color",
"tooltip": "Controls the default background color of the 3D scene. This setting determines the background appearance when a new 3D widget is created, but can be adjusted individually for each widget after creation."
},
"Comfy_Load3D_CameraType": {
"name": "Initial Camera Type",
"tooltip": "Controls whether the camera is perspective or orthographic by default when a new 3D widget is created. This default can still be toggled individually for each widget after creation.",
@@ -116,6 +120,22 @@
"orthographic": "orthographic"
}
},
"Comfy_Load3D_LightAdjustmentIncrement": {
"name": "Light Adjustment Increment",
"tooltip": "Controls the increment size when adjusting light intensity in 3D scenes. A smaller step value allows for finer control over lighting adjustments, while a larger value results in more noticeable changes per adjustment."
},
"Comfy_Load3D_LightIntensity": {
"name": "Initial Light Intensity",
"tooltip": "Sets the default brightness level of lighting in the 3D scene. This value determines how intensely lights illuminate objects when a new 3D widget is created, but can be adjusted individually for each widget after creation."
},
"Comfy_Load3D_LightIntensityMaximum": {
"name": "Light Intensity Maximum",
"tooltip": "Sets the maximum allowable light intensity value for 3D scenes. This defines the upper brightness limit that can be set when adjusting lighting in any 3D widget."
},
"Comfy_Load3D_LightIntensityMinimum": {
"name": "Light Intensity Minimum",
"tooltip": "Sets the minimum allowable light intensity value for 3D scenes. This defines the lower brightness limit that can be set when adjusting lighting in any 3D widget."
},
"Comfy_Load3D_ShowGrid": {
"name": "Initial Grid Visibility",
"tooltip": "Controls whether the grid is visible by default when a new 3D widget is created. This default can still be toggled individually for each widget after creation."
@@ -357,6 +377,10 @@
"LiteGraph_ContextMenu_Scaling": {
"name": "Scale node combo widget menus (lists) when zoomed in"
},
"LiteGraph_Node_DefaultPadding": {
"name": "Always shrink new nodes",
"tooltip": "Resize nodes to the smallest possible size when created. When disabled, a newly added node will be widened slightly to show widget values."
},
"LiteGraph_Node_TooltipDelay": {
"name": "Tooltip Delay"
},

View File

@@ -1,4 +1,7 @@
{
"Comfy-Desktop_CheckForUpdates": {
"label": "Buscar actualizaciones"
},
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
"label": "Abrir carpeta de nodos personalizados"
},
@@ -80,6 +83,9 @@
"Comfy_ClearWorkflow": {
"label": "Borrar flujo de trabajo"
},
"Comfy_ContactSupport": {
"label": "Contactar soporte"
},
"Comfy_DuplicateWorkflow": {
"label": "Duplicar flujo de trabajo actual"
},
@@ -170,6 +176,9 @@
"Comfy_Undo": {
"label": "Deshacer"
},
"Comfy_User_OpenSignInDialog": {
"label": "Abrir diálogo de inicio de sesión"
},
"Workspace_CloseWorkflow": {
"label": "Cerrar Flujo de Trabajo Actual"
},

View File

@@ -17,6 +17,8 @@
"emailPlaceholder": "Ingresa tu correo electrónico",
"failed": "Inicio de sesión fallido",
"forgotPassword": "¿Olvidaste tu contraseña?",
"forgotPasswordError": "No se pudo enviar el correo electrónico para restablecer la contraseña",
"genericErrorMessage": "Lo sentimos, hemos encontrado un error. Por favor, contacta con {supportEmail}.",
"loginButton": "Iniciar sesión",
"loginWithGithub": "Iniciar sesión con Github",
"loginWithGoogle": "Iniciar sesión con Google",
@@ -24,13 +26,21 @@
"orContinueWith": "O continuar con",
"passwordLabel": "Contraseña",
"passwordPlaceholder": "Ingresa tu contraseña",
"passwordResetSent": "Correo electrónico de restablecimiento de contraseña enviado",
"passwordResetSentDetail": "Por favor, revisa tu correo electrónico para encontrar un enlace para restablecer tu contraseña.",
"privacyLink": "Política de privacidad",
"signInOrSignUp": "Iniciar sesión / Registrarse",
"signUp": "Regístrate",
"success": "Inicio de sesión exitoso",
"termsLink": "Términos de uso",
"termsText": "Al hacer clic en \"Siguiente\" o \"Registrarse\", aceptas nuestros",
"title": "Inicia sesión en tu cuenta"
},
"signOut": {
"signOut": "Cerrar sesión",
"success": "Sesión cerrada correctamente",
"successDetail": "Has cerrado sesión en tu cuenta."
},
"signup": {
"alreadyHaveAccount": "¿Ya tienes una cuenta?",
"emailLabel": "Correo electrónico",
@@ -92,6 +102,23 @@
"Title": "Título",
"Unpin": "Desanclar"
},
"credits": {
"credits": "Créditos",
"faqs": "Preguntas frecuentes",
"invoiceHistory": "Historial de facturas",
"lastUpdated": "Última actualización",
"messageSupport": "Contactar soporte",
"purchaseCredits": "Comprar créditos",
"topUp": {
"buyNow": "Comprar ahora",
"insufficientMessage": "No tienes suficientes créditos para ejecutar este flujo de trabajo.",
"insufficientTitle": "Créditos insuficientes",
"maxAmount": "(Máx. $1,000 USD)",
"quickPurchase": "Compra rápida",
"seeDetails": "Ver detalles"
},
"yourCreditBalance": "Tu saldo de créditos"
},
"dataTypes": {
"AUDIO": "AUDIO",
"BOOLEAN": "BOOLEANO",
@@ -137,7 +164,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 +195,7 @@
"about": "Acerca de",
"add": "Añadir",
"all": "Todo",
"amount": "Cantidad",
"apply": "Aplicar",
"back": "Atrás",
"cancel": "Cancelar",
@@ -390,19 +420,34 @@
},
"issueReport": {
"contactFollowUp": "Contáctame para seguimiento",
"contactSupportDescription": "Por favor, complete el siguiente formulario con su reporte",
"contactSupportTitle": "Contactar Soporte",
"describeTheProblem": "Describa el problema",
"email": "Correo electrónico",
"feedbackTitle": "Ayúdanos a mejorar ComfyUI proporcionando comentarios",
"helpFix": "Ayuda a Solucionar Esto",
"helpTypes": {
"billingPayments": "Facturación / Pagos",
"bugReport": "Reporte de error",
"giveFeedback": "Enviar comentarios",
"loginAccessIssues": "Problemas de inicio de sesión / acceso",
"somethingElse": "Otro"
},
"notifyResolve": "Notifícame cuando se resuelva",
"provideAdditionalDetails": "Proporciona detalles adicionales (opcional)",
"provideEmail": "Danos tu correo electrónico (opcional)",
"rating": "Calificación",
"selectIssue": "Seleccione el problema",
"stackTrace": "Rastreo de Pila",
"submitErrorReport": "Enviar Reporte de Error (Opcional)",
"systemStats": "Estadísticas del Sistema",
"validation": {
"invalidEmail": "Por favor ingresa una dirección de correo electrónico válida",
"maxLength": "Mensaje demasiado largo"
}
"maxLength": "Mensaje demasiado largo",
"selectIssueType": "Por favor, seleccione un tipo de problema"
},
"whatCanWeInclude": "Especifique qué incluir en el reporte",
"whatDoYouNeedHelpWith": "¿Con qué necesita ayuda?"
},
"load3d": {
"applyingTexture": "Aplicando textura...",
@@ -565,6 +610,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",
@@ -574,6 +620,7 @@
"ComfyUI Docs": "Documentos de ComfyUI",
"ComfyUI Forum": "Foro de ComfyUI",
"ComfyUI Issues": "Problemas de ComfyUI",
"Contact Support": "Contactar soporte",
"Convert selected nodes to group node": "Convertir nodos seleccionados en nodo de grupo",
"Custom Nodes Manager": "Gestor de nodos personalizados",
"Delete Selected Items": "Eliminar elementos seleccionados",
@@ -600,6 +647,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",
@@ -869,6 +917,7 @@
"Comfy": "Comfy",
"Comfy-Desktop": "Comfy-Desktop",
"ContextMenu": "Menú Contextual",
"Credits": "Créditos",
"CustomColorPalettes": "Paletas de Colores Personalizadas",
"DevMode": "Modo de Desarrollo",
"EditTokenWeight": "Editar Peso del Token",
@@ -877,6 +926,7 @@
"Graph": "Gráfico",
"Group": "Grupo",
"Keybinding": "Asignación de Teclas",
"Light": "Claro",
"Link": "Enlace",
"LinkRelease": "Liberación de Enlace",
"LiteGraph": "Lite Graph",
@@ -902,6 +952,7 @@
"Sidebar": "Barra Lateral",
"Tree Explorer": "Explorador de Árbol",
"UV": "UV",
"User": "Usuario",
"Validation": "Validación",
"Window": "Ventana",
"Workflow": "Flujo de Trabajo"
@@ -969,8 +1020,7 @@
"ControlNet": "ControlNet",
"Custom Nodes": "Nodos Personalizados",
"Flux": "Flux",
"SD3_5": "SD3.5",
"SDXL": "SDXL",
"Image": "Imagen",
"Upscaling": "Ampliación",
"Video": "Video"
},
@@ -1015,13 +1065,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 +1093,9 @@
"ltxv_text_to_video": "LTXV Texto a Video",
"mochi_text_to_video_example": "Mochi Texto a Video",
"text_to_video_wan": "Wan 2.1 Texto a Video",
"txt_to_image_to_video": "SVD Texto a Imagen a Video"
"txt_to_image_to_video": "SVD Texto a Imagen a Video",
"wan2_1_fun_control": "Wan 2.1 ControlNet",
"wan2_1_fun_inp": "Wan 2.1 Relleno"
}
},
"title": "Comienza con una Plantilla"
@@ -1079,6 +1132,17 @@
"next": "Siguiente",
"selectUser": "Selecciona un usuario"
},
"userSettings": {
"email": "Correo electrónico",
"name": "Nombre",
"notSet": "No establecido",
"provider": "Método de inicio de sesión",
"providers": {
"github": "GitHub",
"google": "Google"
},
"title": "Configuración de usuario"
},
"validation": {
"invalidEmail": "Dirección de correo electrónico inválida",
"maxLength": "No debe tener más de {length} caracteres",

View File

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

View File

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

View File

@@ -17,6 +17,8 @@
"emailPlaceholder": "Entrez votre email",
"failed": "Échec de la connexion",
"forgotPassword": "Mot de passe oublié?",
"forgotPasswordError": "Échec de l'envoi de l'e-mail de réinitialisation du mot de passe",
"genericErrorMessage": "Désolé, une erreur s'est produite. Veuillez contacter {supportEmail}.",
"loginButton": "Se connecter",
"loginWithGithub": "Se connecter avec Github",
"loginWithGoogle": "Se connecter avec Google",
@@ -24,13 +26,21 @@
"orContinueWith": "Ou continuer avec",
"passwordLabel": "Mot de passe",
"passwordPlaceholder": "Entrez votre mot de passe",
"passwordResetSent": "E-mail de réinitialisation du mot de passe envoyé",
"passwordResetSentDetail": "Veuillez vérifier votre e-mail pour un lien de réinitialisation de votre mot de passe.",
"privacyLink": "Politique de confidentialité",
"signInOrSignUp": "Se connecter / Sinscrire",
"signUp": "S'inscrire",
"success": "Connexion réussie",
"termsLink": "Conditions d'utilisation",
"termsText": "En cliquant sur \"Suivant\" ou \"S'inscrire\", vous acceptez nos",
"title": "Connectez-vous à votre compte"
},
"signOut": {
"signOut": "Se déconnecter",
"success": "Déconnexion réussie",
"successDetail": "Vous avez été déconnecté de votre compte."
},
"signup": {
"alreadyHaveAccount": "Vous avez déjà un compte?",
"emailLabel": "Email",
@@ -92,6 +102,23 @@
"Title": "Titre",
"Unpin": "Désépingler"
},
"credits": {
"credits": "Crédits",
"faqs": "FAQ",
"invoiceHistory": "Historique des factures",
"lastUpdated": "Dernière mise à jour",
"messageSupport": "Contacter le support",
"purchaseCredits": "Acheter des crédits",
"topUp": {
"buyNow": "Acheter maintenant",
"insufficientMessage": "Vous n'avez pas assez de crédits pour exécuter ce workflow.",
"insufficientTitle": "Crédits insuffisants",
"maxAmount": "(Max. 1 000 $ US)",
"quickPurchase": "Achat rapide",
"seeDetails": "Voir les détails"
},
"yourCreditBalance": "Votre solde de crédits"
},
"dataTypes": {
"AUDIO": "AUDIO",
"BOOLEAN": "BOOLEAN",
@@ -137,7 +164,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 +195,7 @@
"about": "À propos",
"add": "Ajouter",
"all": "Tout",
"amount": "Quantité",
"apply": "Appliquer",
"back": "Retour",
"cancel": "Annuler",
@@ -390,19 +420,34 @@
},
"issueReport": {
"contactFollowUp": "Contactez-moi pour un suivi",
"contactSupportDescription": "Veuillez remplir le formulaire ci-dessous avec votre signalement",
"contactSupportTitle": "Contacter le support",
"describeTheProblem": "Décrivez le problème",
"email": "E-mail",
"feedbackTitle": "Aidez-nous à améliorer ComfyUI en fournissant des commentaires",
"helpFix": "Aidez à résoudre cela",
"helpTypes": {
"billingPayments": "Facturation / Paiements",
"bugReport": "Signaler un bug",
"giveFeedback": "Donner un avis",
"loginAccessIssues": "Problèmes de connexion / d'accès",
"somethingElse": "Autre chose"
},
"notifyResolve": "Prévenez-moi lorsque résolu",
"provideAdditionalDetails": "Fournir des détails supplémentaires (facultatif)",
"provideEmail": "Donnez-nous votre email (Facultatif)",
"rating": "Évaluation",
"selectIssue": "Sélectionnez le problème",
"stackTrace": "Trace de la pile",
"submitErrorReport": "Soumettre un rapport d'erreur (Facultatif)",
"systemStats": "Statistiques du système",
"validation": {
"invalidEmail": "Veuillez entrer une adresse e-mail valide",
"maxLength": "Message trop long"
}
"maxLength": "Message trop long",
"selectIssueType": "Veuillez sélectionner un type de problème"
},
"whatCanWeInclude": "Précisez ce qu'il faut inclure dans le rapport",
"whatDoYouNeedHelpWith": "Avec quoi avez-vous besoin d'aide ?"
},
"load3d": {
"applyingTexture": "Application de la texture...",
@@ -565,6 +610,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",
@@ -574,6 +620,7 @@
"ComfyUI Docs": "Docs de ComfyUI",
"ComfyUI Forum": "Forum ComfyUI",
"ComfyUI Issues": "Problèmes de ComfyUI",
"Contact Support": "Contacter le support",
"Convert selected nodes to group node": "Convertir les nœuds sélectionnés en nœud de groupe",
"Custom Nodes Manager": "Gestionnaire de Nœuds Personnalisés",
"Delete Selected Items": "Supprimer les éléments sélectionnés",
@@ -600,6 +647,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",
@@ -869,6 +917,7 @@
"Comfy": "Confort",
"Comfy-Desktop": "Comfy-Desktop",
"ContextMenu": "Menu Contextuel",
"Credits": "Crédits",
"CustomColorPalettes": "Palettes de Couleurs Personnalisées",
"DevMode": "Mode Développeur",
"EditTokenWeight": "Modifier le Poids du Jeton",
@@ -877,6 +926,7 @@
"Graph": "Graphique",
"Group": "Groupe",
"Keybinding": "Raccourci Clavier",
"Light": "Clair",
"Link": "Lien",
"LinkRelease": "Libération de Lien",
"LiteGraph": "Lite Graph",
@@ -902,6 +952,7 @@
"Sidebar": "Barre Latérale",
"Tree Explorer": "Explorateur d'Arbre",
"UV": "UV",
"User": "Utilisateur",
"Validation": "Validation",
"Window": "Fenêtre",
"Workflow": "Flux de Travail"
@@ -969,8 +1020,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 +1065,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 +1093,9 @@
"ltxv_text_to_video": "LTXV Texte à Vidéo",
"mochi_text_to_video_example": "Exemple de Texte à Vidéo Mochi",
"text_to_video_wan": "Wan 2.1 Texte à Vidéo",
"txt_to_image_to_video": "Texte à Image à Vidéo"
"txt_to_image_to_video": "Texte à Image à Vidéo",
"wan2_1_fun_control": "Wan 2.1 ControlNet",
"wan2_1_fun_inp": "Wan 2.1 Inpainting"
}
},
"title": "Commencez avec un modèle"
@@ -1079,6 +1132,17 @@
"next": "Suivant",
"selectUser": "Sélectionnez un utilisateur"
},
"userSettings": {
"email": "E-mail",
"name": "Nom",
"notSet": "Non défini",
"provider": "Méthode de connexion",
"providers": {
"github": "GitHub",
"google": "Google"
},
"title": "Paramètres utilisateur"
},
"validation": {
"invalidEmail": "Adresse e-mail invalide",
"maxLength": "Ne doit pas dépasser {length} caractères",

View File

@@ -108,6 +108,10 @@
"Straight": "Droit"
}
},
"Comfy_Load3D_BackgroundColor": {
"name": "Couleur de fond initiale",
"tooltip": "Contrôle la couleur de fond par défaut de la scène 3D. Ce paramètre détermine l'apparence du fond lors de la création d'un nouveau widget 3D, mais peut être ajusté individuellement pour chaque widget après la création."
},
"Comfy_Load3D_CameraType": {
"name": "Type de Caméra",
"options": {
@@ -116,6 +120,22 @@
},
"tooltip": "Contrôle si la caméra est en perspective ou orthographique par défaut lorsqu'un nouveau widget 3D est créé. Ce défaut peut toujours être basculé individuellement pour chaque widget après sa création."
},
"Comfy_Load3D_LightAdjustmentIncrement": {
"name": "Incrément d'ajustement de la lumière",
"tooltip": "Contrôle la taille de l'incrément lors de l'ajustement de l'intensité lumineuse dans les scènes 3D. Une valeur de pas plus petite permet un contrôle plus précis des ajustements de lumière, tandis qu'une valeur plus grande entraîne des changements plus visibles à chaque ajustement."
},
"Comfy_Load3D_LightIntensity": {
"name": "Intensité lumineuse initiale",
"tooltip": "Définit le niveau de luminosité par défaut de léclairage dans la scène 3D. Cette valeur détermine lintensité avec laquelle les lumières illuminent les objets lors de la création dun nouveau widget 3D, mais peut être ajustée individuellement pour chaque widget après la création."
},
"Comfy_Load3D_LightIntensityMaximum": {
"name": "Intensité lumineuse maximale",
"tooltip": "Définit la valeur maximale autorisée pour lintensité lumineuse dans les scènes 3D. Cela fixe la limite supérieure de luminosité pouvant être réglée lors de lajustement de léclairage dans tout widget 3D."
},
"Comfy_Load3D_LightIntensityMinimum": {
"name": "Intensité lumineuse minimale",
"tooltip": "Définit la valeur minimale autorisée de lintensité lumineuse pour les scènes 3D. Cela définit la limite inférieure de luminosité pouvant être réglée lors de lajustement de léclairage dans tout widget 3D."
},
"Comfy_Load3D_ShowGrid": {
"name": "Afficher la Grille",
"tooltip": "Basculer pour afficher la grille par défaut"
@@ -357,6 +377,10 @@
"LiteGraph_ContextMenu_Scaling": {
"name": "Mise à l'échelle des menus de widgets combinés de nœuds (listes) lors du zoom"
},
"LiteGraph_Node_DefaultPadding": {
"name": "Toujours réduire les nouveaux nœuds",
"tooltip": "Redimensionner les nœuds à la taille minimale possible lors de leur création. Lorsque cette option est désactivée, un nœud nouvellement ajouté sera légèrement élargi pour afficher les valeurs des widgets."
},
"LiteGraph_Node_TooltipDelay": {
"name": "Délai d'infobulle"
},

View File

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

View File

@@ -17,6 +17,8 @@
"emailPlaceholder": "メールアドレスを入力してください",
"failed": "ログイン失敗",
"forgotPassword": "パスワードを忘れましたか?",
"forgotPasswordError": "パスワードリセット用メールの送信に失敗しました",
"genericErrorMessage": "申し訳ありませんが、エラーが発生しました。{supportEmail} までご連絡ください。",
"loginButton": "ログイン",
"loginWithGithub": "Githubでログイン",
"loginWithGoogle": "Googleでログイン",
@@ -24,13 +26,21 @@
"orContinueWith": "または以下で続ける",
"passwordLabel": "パスワード",
"passwordPlaceholder": "パスワードを入力してください",
"passwordResetSent": "パスワードリセット用メールを送信しました",
"passwordResetSentDetail": "パスワードをリセットするためのリンクが記載されたメールをご確認ください。",
"privacyLink": "プライバシーポリシー",
"signInOrSignUp": "サインイン / サインアップ",
"signUp": "サインアップ",
"success": "ログイン成功",
"termsLink": "利用規約",
"termsText": "「次へ」または「サインアップ」をクリックすると、私たちの",
"title": "アカウントにログインする"
},
"signOut": {
"signOut": "ログアウト",
"success": "正常にサインアウトしました",
"successDetail": "アカウントからサインアウトしました。"
},
"signup": {
"alreadyHaveAccount": "すでにアカウントをお持ちですか?",
"emailLabel": "メール",
@@ -92,6 +102,23 @@
"Title": "タイトル",
"Unpin": "ピンを解除"
},
"credits": {
"credits": "クレジット",
"faqs": "よくある質問",
"invoiceHistory": "請求履歴",
"lastUpdated": "最終更新",
"messageSupport": "サポートにメッセージ",
"purchaseCredits": "クレジットを購入",
"topUp": {
"buyNow": "今すぐ購入",
"insufficientMessage": "このワークフローを実行するのに十分なクレジットがありません。",
"insufficientTitle": "クレジット不足",
"maxAmount": "(最大 $1,000 USD",
"quickPurchase": "クイック購入",
"seeDetails": "詳細を見る"
},
"yourCreditBalance": "あなたのクレジット残高"
},
"dataTypes": {
"AUDIO": "オーディオ",
"BOOLEAN": "ブール",
@@ -137,7 +164,9 @@
"desktopUpdate": {
"description": "ComfyUIデスクトップは新しい依存関係をインストールしています。これには数分かかる場合があります。",
"terminalDefaultMessage": "更新からの任意のコンソール出力はここに表示されます。",
"title": "ComfyUIデスクトップの更新"
"title": "ComfyUIデスクトップの更新",
"updateAvailableMessage": "アップデートが利用可能です。今すぐ再起動してアップデートしますか?",
"updateFoundTitle": "アップデートが見つかりました (v{version})"
},
"downloadGit": {
"gitWebsite": "Gitをダウンロード",
@@ -166,6 +195,7 @@
"about": "情報",
"add": "追加",
"all": "すべて",
"amount": "量",
"apply": "適用する",
"back": "戻る",
"cancel": "キャンセル",
@@ -390,19 +420,34 @@
},
"issueReport": {
"contactFollowUp": "フォローアップのために私に連絡する",
"contactSupportDescription": "下記のフォームにご報告内容をご記入ください",
"contactSupportTitle": "サポートに連絡",
"describeTheProblem": "問題の内容を記述してください",
"email": "メールアドレス",
"feedbackTitle": "フィードバックを提供してComfyUIの改善にご協力ください",
"helpFix": "これを修正するのを助ける",
"helpTypes": {
"billingPayments": "請求/支払い",
"bugReport": "バグ報告",
"giveFeedback": "フィードバックを送る",
"loginAccessIssues": "ログイン/アクセスの問題",
"somethingElse": "その他"
},
"notifyResolve": "解決したときに通知する",
"provideAdditionalDetails": "追加の詳細を提供する(オプション)",
"provideEmail": "あなたのメールアドレスを教えてください(オプション)",
"rating": "評価",
"selectIssue": "問題を選択してください",
"stackTrace": "スタックトレース",
"submitErrorReport": "エラーレポートを提出する(オプション)",
"systemStats": "システム統計",
"validation": {
"invalidEmail": "有効なメールアドレスを入力してください",
"maxLength": "メッセージが長すぎます"
}
"maxLength": "メッセージが長すぎます",
"selectIssueType": "問題の種類を選択してください"
},
"whatCanWeInclude": "レポートに含める内容を指定してください",
"whatDoYouNeedHelpWith": "どのようなサポートが必要ですか?"
},
"load3d": {
"applyingTexture": "テクスチャを適用中...",
@@ -565,6 +610,7 @@
"Bypass/Unbypass Selected Nodes": "選択したノードのバイパス/バイパス解除",
"Canvas Toggle Link Visibility": "キャンバスのリンク表示を切り替え",
"Canvas Toggle Lock": "キャンバスのロックを切り替え",
"Check for Updates": "更新を確認する",
"Clear Pending Tasks": "保留中のタスクをクリア",
"Clear Workflow": "ワークフローをクリア",
"Clipspace": "クリップスペース",
@@ -574,6 +620,7 @@
"ComfyUI Docs": "ComfyUIのドキュメント",
"ComfyUI Forum": "ComfyUI フォーラム",
"ComfyUI Issues": "ComfyUIの問題",
"Contact Support": "サポートに連絡",
"Convert selected nodes to group node": "選択したノードをグループノードに変換",
"Custom Nodes Manager": "カスタムノードマネージャ",
"Delete Selected Items": "選択したアイテムを削除",
@@ -600,6 +647,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": "選択したノードのピン留め/ピン留め解除",
@@ -869,6 +917,7 @@
"Comfy": "Comfy",
"Comfy-Desktop": "Comfyデスクトップ",
"ContextMenu": "コンテキストメニュー",
"Credits": "クレジット",
"CustomColorPalettes": "カスタムカラーパレット",
"DevMode": "開発モード",
"EditTokenWeight": "トークンの重みを編集",
@@ -877,6 +926,7 @@
"Graph": "グラフ",
"Group": "グループ",
"Keybinding": "キー割り当て",
"Light": "ライト",
"Link": "リンク",
"LinkRelease": "リンク解除",
"LiteGraph": "Lite Graph",
@@ -902,6 +952,7 @@
"Sidebar": "サイドバー",
"Tree Explorer": "ツリーエクスプローラー",
"UV": "UV",
"User": "ユーザー",
"Validation": "検証",
"Window": "ウィンドウ",
"Workflow": "ワークフロー"
@@ -969,8 +1020,7 @@
"ControlNet": "ControlNet",
"Custom Nodes": "カスタムノード",
"Flux": "Flux",
"SD3_5": "SD3.5",
"SDXL": "SDXL",
"Image": "画像",
"Upscaling": "アップスケーリング",
"Video": "ビデオ"
},
@@ -1015,13 +1065,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 +1093,9 @@
"ltxv_text_to_video": "LTXVテキストからビデオへ",
"mochi_text_to_video_example": "Mochiテキストからビデオへ",
"text_to_video_wan": "Wan 2.1 テキストからビデオへ",
"txt_to_image_to_video": "テキストから画像へ、画像からビデオへ"
"txt_to_image_to_video": "テキストから画像へ、画像からビデオへ",
"wan2_1_fun_control": "Wan 2.1 ControlNet",
"wan2_1_fun_inp": "Wan 2.1 インペインティング"
}
},
"title": "テンプレートを利用して開始"
@@ -1079,6 +1132,17 @@
"next": "次へ",
"selectUser": "ユーザーを選択"
},
"userSettings": {
"email": "メールアドレス",
"name": "名前",
"notSet": "未設定",
"provider": "サインイン方法",
"providers": {
"github": "GitHub",
"google": "Google"
},
"title": "ユーザー設定"
},
"validation": {
"invalidEmail": "無効なメールアドレス",
"maxLength": "{length}文字以下でなければなりません",

View File

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

View File

@@ -1,4 +1,7 @@
{
"Comfy-Desktop_CheckForUpdates": {
"label": "업데이트 확인"
},
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
"label": "사용자 정의 노드 폴더 열기"
},
@@ -80,6 +83,9 @@
"Comfy_ClearWorkflow": {
"label": "워크플로 지우기"
},
"Comfy_ContactSupport": {
"label": "지원팀에 문의하기"
},
"Comfy_DuplicateWorkflow": {
"label": "현재 워크플로우 복제"
},
@@ -170,6 +176,9 @@
"Comfy_Undo": {
"label": "실행 취소"
},
"Comfy_User_OpenSignInDialog": {
"label": "로그인 대화상자 열기"
},
"Workspace_CloseWorkflow": {
"label": "현재 워크플로우 닫기"
},

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