Compare commits
89 Commits
export-gen
...
v1.18.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcbdee54ec | ||
|
|
a944372f39 | ||
|
|
df51e89311 | ||
|
|
b314435f81 | ||
|
|
ba367c0214 | ||
|
|
64ad6a9bb0 | ||
|
|
3819db5ec4 | ||
|
|
80517e8204 | ||
|
|
40034e77f9 | ||
|
|
2ef8b7cfd7 | ||
|
|
9cf3a0e568 | ||
|
|
f562cf27cd | ||
|
|
4244a0a258 | ||
|
|
ad3d2fe2e9 | ||
|
|
4c23cfbd4d | ||
|
|
9e10e55633 | ||
|
|
59cbe90fd3 | ||
|
|
16bd9abccd | ||
|
|
e84bdc96cf | ||
|
|
a57be36d4d | ||
|
|
3bd508c001 | ||
|
|
612500a4dc | ||
|
|
e9723407d8 | ||
|
|
a01aa39423 | ||
|
|
ab94a55858 | ||
|
|
1bcf5e28d4 | ||
|
|
9e247063aa | ||
|
|
cdddf359a8 | ||
|
|
8558f87547 | ||
|
|
262991db6b | ||
|
|
585d52e24e | ||
|
|
b7535755f0 | ||
|
|
6b7b0f6ec1 | ||
|
|
c7318bcf0a | ||
|
|
11f909436c | ||
|
|
d8f4dc95bb | ||
|
|
c1bc664edd | ||
|
|
e7fe2046ba | ||
|
|
bf4ad38e9b | ||
|
|
2b024bb186 | ||
|
|
6e5930c355 | ||
|
|
6151d487c6 | ||
|
|
e027a9bf44 | ||
|
|
53ee5904e8 | ||
|
|
f82bb71b1e | ||
|
|
40d08a890d | ||
|
|
ebf3c0c049 | ||
|
|
e77d5c1f57 | ||
|
|
b5c1da22db | ||
|
|
0006dd3855 | ||
|
|
7355209c12 | ||
|
|
2aef0a9af8 | ||
|
|
b74887d543 | ||
|
|
bf4ae227b3 | ||
|
|
184bb582da | ||
|
|
3bc3179763 | ||
|
|
eb100894ce | ||
|
|
9a992cb14d | ||
|
|
133aa9bc87 | ||
|
|
3204637e5a | ||
|
|
b2cb719026 | ||
|
|
add805460c | ||
|
|
9621b8f339 | ||
|
|
6be381b15d | ||
|
|
8afe99f48c | ||
|
|
9cd11261f9 | ||
|
|
fbc6665ff4 | ||
|
|
2daa51421c | ||
|
|
0f175c3dc1 | ||
|
|
8d4263c94e | ||
|
|
04580ac031 | ||
|
|
cd35f1d86d | ||
|
|
5d584577fe | ||
|
|
10a96d1af6 | ||
|
|
03392a3cc7 | ||
|
|
12576243ad | ||
|
|
e2a6dc2ec8 | ||
|
|
2f77d74891 | ||
|
|
dacb59f5d3 | ||
|
|
74f991ec1b | ||
|
|
6bc03a624e | ||
|
|
1fb015e046 | ||
|
|
87bf2310b6 | ||
|
|
f1a25989d7 | ||
|
|
236e3fb3e9 | ||
|
|
50382827bc | ||
|
|
41675805b6 | ||
|
|
6321fae6f3 | ||
|
|
06caa21a4d |
2
.github/workflows/release.yaml
vendored
@@ -29,9 +29,9 @@ jobs:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
|
||||
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
|
||||
USE_PROD_CONFIG: 'true'
|
||||
run: |
|
||||
npm ci
|
||||
npm run fetch-templates
|
||||
npm run build
|
||||
npm run zipdist
|
||||
- name: Upload dist artifact
|
||||
|
||||
3
.github/workflows/test-ui.yaml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
with:
|
||||
repository: 'Comfy-Org/ComfyUI_devtools'
|
||||
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
|
||||
ref: '49c8220be49120dbaff85f32813d854d6dff2d05'
|
||||
ref: '9d2421fd3208a310e4d0f71fca2ea0c985759c33'
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -39,7 +39,6 @@ jobs:
|
||||
- name: Build ComfyUI_frontend
|
||||
run: |
|
||||
npm ci
|
||||
npm run fetch-templates
|
||||
npm run build
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
const { defineConfig } = require('@lobehub/i18n-cli');
|
||||
|
||||
module.exports = defineConfig({
|
||||
modelName: 'gpt-4',
|
||||
modelName: 'gpt-4.1',
|
||||
splitToken: 1024,
|
||||
entry: 'src/locales/en',
|
||||
entryLocale: 'en',
|
||||
output: 'src/locales',
|
||||
outputLocales: ['zh', 'ru', 'ja', 'ko', 'fr', 'es'],
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora.
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
|
||||
'latent' is the short form of 'latent space'.
|
||||
'mask' is in the context of image processing.
|
||||
`
|
||||
|
||||
BIN
browser_tests/assets/animated_webp.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
11
browser_tests/assets/widgets/load_animated_webp.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"8": {
|
||||
"inputs": {
|
||||
"image": "animated_web.webp"
|
||||
},
|
||||
"class_type": "DevToolsLoadAnimatedImageTest",
|
||||
"_meta": {
|
||||
"title": "Load Animated Image"
|
||||
}
|
||||
}
|
||||
}
|
||||
60
browser_tests/assets/widgets/save_animated_webp.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"id": "3f1fcbf9-f9de-4935-8fad-401813f61b13",
|
||||
"revision": 0,
|
||||
"last_node_id": 10,
|
||||
"last_link_id": 4,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 9,
|
||||
"type": "SaveAnimatedWEBP",
|
||||
"pos": [336, 104],
|
||||
"size": [210, 368],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 4
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": ["ComfyUI", 6, true, 80, "default"]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "DevToolsLoadAnimatedImageTest",
|
||||
"pos": [64, 104],
|
||||
"size": [210, 316],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [4]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "DevToolsLoadAnimatedImageTest"
|
||||
},
|
||||
"widgets_values": ["animated_web.webp", "image"]
|
||||
}
|
||||
],
|
||||
"links": [[4, 10, 0, 9, 0, "IMAGE"]],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"frontendVersion": "1.17.0"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -463,87 +463,128 @@ export class ComfyPage {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async dragAndDropFile(
|
||||
fileName: string,
|
||||
async dragAndDropExternalResource(
|
||||
options: {
|
||||
fileName?: string
|
||||
url?: string
|
||||
dropPosition?: Position
|
||||
} = {}
|
||||
) {
|
||||
const { dropPosition = { x: 100, y: 100 } } = options
|
||||
const { dropPosition = { x: 100, y: 100 }, fileName, url } = options
|
||||
|
||||
const filePath = this.assetPath(fileName)
|
||||
if (!fileName && !url)
|
||||
throw new Error('Must provide either fileName or url')
|
||||
|
||||
// Read the file content
|
||||
const buffer = fs.readFileSync(filePath)
|
||||
const evaluateParams: {
|
||||
dropPosition: Position
|
||||
fileName?: string
|
||||
fileType?: string
|
||||
buffer?: Uint8Array | number[]
|
||||
url?: string
|
||||
} = { dropPosition }
|
||||
|
||||
// Get file type
|
||||
const getFileType = (fileName: string) => {
|
||||
if (fileName.endsWith('.png')) return 'image/png'
|
||||
if (fileName.endsWith('.webp')) return 'image/webp'
|
||||
if (fileName.endsWith('.webm')) return 'video/webm'
|
||||
if (fileName.endsWith('.json')) return 'application/json'
|
||||
if (fileName.endsWith('.glb')) return 'model/gltf-binary'
|
||||
return 'application/octet-stream'
|
||||
// Dropping a file from the filesystem
|
||||
if (fileName) {
|
||||
const filePath = this.assetPath(fileName)
|
||||
const buffer = fs.readFileSync(filePath)
|
||||
|
||||
const getFileType = (fileName: string) => {
|
||||
if (fileName.endsWith('.png')) return 'image/png'
|
||||
if (fileName.endsWith('.webp')) return 'image/webp'
|
||||
if (fileName.endsWith('.webm')) return 'video/webm'
|
||||
if (fileName.endsWith('.json')) return 'application/json'
|
||||
if (fileName.endsWith('.glb')) return 'model/gltf-binary'
|
||||
return 'application/octet-stream'
|
||||
}
|
||||
|
||||
evaluateParams.fileName = fileName
|
||||
evaluateParams.fileType = getFileType(fileName)
|
||||
evaluateParams.buffer = [...new Uint8Array(buffer)]
|
||||
}
|
||||
|
||||
const fileType = getFileType(fileName)
|
||||
// Dropping a URL (e.g., dropping image across browser tabs in Firefox)
|
||||
if (url) evaluateParams.url = url
|
||||
|
||||
await this.page.evaluate(
|
||||
async ({ buffer, fileName, fileType, dropPosition }) => {
|
||||
const file = new File([new Uint8Array(buffer)], fileName, {
|
||||
type: fileType
|
||||
})
|
||||
const dataTransfer = new DataTransfer()
|
||||
dataTransfer.items.add(file)
|
||||
// Execute the drag and drop in the browser
|
||||
await this.page.evaluate(async (params) => {
|
||||
const dataTransfer = new DataTransfer()
|
||||
|
||||
const targetElement = document.elementFromPoint(
|
||||
dropPosition.x,
|
||||
dropPosition.y
|
||||
)
|
||||
|
||||
if (!targetElement) {
|
||||
console.error('No element found at drop position:', dropPosition)
|
||||
return { success: false, error: 'No element at position' }
|
||||
}
|
||||
|
||||
const eventOptions = {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
dataTransfer,
|
||||
clientX: dropPosition.x,
|
||||
clientY: dropPosition.y
|
||||
}
|
||||
|
||||
const dragOverEvent = new DragEvent('dragover', eventOptions)
|
||||
const dropEvent = new DragEvent('drop', eventOptions)
|
||||
|
||||
Object.defineProperty(dropEvent, 'preventDefault', {
|
||||
value: () => {},
|
||||
writable: false
|
||||
})
|
||||
Object.defineProperty(dropEvent, 'stopPropagation', {
|
||||
value: () => {},
|
||||
writable: false
|
||||
})
|
||||
|
||||
targetElement.dispatchEvent(dragOverEvent)
|
||||
targetElement.dispatchEvent(dropEvent)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
targetInfo: {
|
||||
tagName: targetElement.tagName,
|
||||
id: targetElement.id,
|
||||
classList: Array.from(targetElement.classList)
|
||||
// Add file if provided
|
||||
if (params.buffer && params.fileName && params.fileType) {
|
||||
const file = new File(
|
||||
[new Uint8Array(params.buffer)],
|
||||
params.fileName,
|
||||
{
|
||||
type: params.fileType
|
||||
}
|
||||
)
|
||||
dataTransfer.items.add(file)
|
||||
}
|
||||
|
||||
// Add URL data if provided
|
||||
if (params.url) {
|
||||
dataTransfer.setData('text/uri-list', params.url)
|
||||
dataTransfer.setData('text/x-moz-url', params.url)
|
||||
}
|
||||
|
||||
const targetElement = document.elementFromPoint(
|
||||
params.dropPosition.x,
|
||||
params.dropPosition.y
|
||||
)
|
||||
|
||||
if (!targetElement) {
|
||||
console.error('No element found at drop position:', params.dropPosition)
|
||||
return { success: false, error: 'No element at position' }
|
||||
}
|
||||
|
||||
const eventOptions = {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
dataTransfer,
|
||||
clientX: params.dropPosition.x,
|
||||
clientY: params.dropPosition.y
|
||||
}
|
||||
|
||||
const dragOverEvent = new DragEvent('dragover', eventOptions)
|
||||
const dropEvent = new DragEvent('drop', eventOptions)
|
||||
|
||||
Object.defineProperty(dropEvent, 'preventDefault', {
|
||||
value: () => {},
|
||||
writable: false
|
||||
})
|
||||
|
||||
Object.defineProperty(dropEvent, 'stopPropagation', {
|
||||
value: () => {},
|
||||
writable: false
|
||||
})
|
||||
|
||||
targetElement.dispatchEvent(dragOverEvent)
|
||||
targetElement.dispatchEvent(dropEvent)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
targetInfo: {
|
||||
tagName: targetElement.tagName,
|
||||
id: targetElement.id,
|
||||
classList: Array.from(targetElement.classList)
|
||||
}
|
||||
},
|
||||
{ buffer: [...new Uint8Array(buffer)], fileName, fileType, dropPosition }
|
||||
)
|
||||
}
|
||||
}, evaluateParams)
|
||||
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async dragAndDropFile(
|
||||
fileName: string,
|
||||
options: { dropPosition?: Position } = {}
|
||||
) {
|
||||
return this.dragAndDropExternalResource({ fileName, ...options })
|
||||
}
|
||||
|
||||
async dragAndDropURL(url: string, options: { dropPosition?: Position } = {}) {
|
||||
return this.dragAndDropExternalResource({ url, ...options })
|
||||
}
|
||||
|
||||
async dragNode2() {
|
||||
await this.dragAndDrop({ x: 622, y: 400 }, { x: 622, y: 300 })
|
||||
await this.nextFrame()
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
After Width: | Height: | Size: 83 KiB |
@@ -3,17 +3,35 @@ import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Load Workflow in Media', () => {
|
||||
;[
|
||||
const fileNames = [
|
||||
'workflow.webp',
|
||||
'edited_workflow.webp',
|
||||
'no_workflow.webp',
|
||||
'large_workflow.webp',
|
||||
'workflow.webm',
|
||||
'workflow.glb'
|
||||
].forEach(async (fileName) => {
|
||||
test(`Load workflow in ${fileName}`, async ({ comfyPage }) => {
|
||||
]
|
||||
fileNames.forEach(async (fileName) => {
|
||||
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.dragAndDropFile(fileName)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
|
||||
})
|
||||
})
|
||||
|
||||
const urls = [
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/hidream/hidream_dev_example.png'
|
||||
]
|
||||
urls.forEach(async (url) => {
|
||||
test(`Load workflow from URL${url} (drop from different browser tabs)`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.dragAndDropURL(url)
|
||||
const readableName = url.split('/').pop()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
`dropped_workflow_url_${readableName}.png`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
After Width: | Height: | Size: 141 KiB |
@@ -92,4 +92,20 @@ test.describe('Node badge color', () => {
|
||||
'node-badge-unknown-color-palette.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can show node badge with light color palette', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting(
|
||||
'Comfy.NodeBadge.NodeIdBadgeMode',
|
||||
NodeBadgeMode.ShowAll
|
||||
)
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
|
||||
await comfyPage.nextFrame()
|
||||
// Click empty space to trigger canvas re-render.
|
||||
await comfyPage.clickEmptySpace()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'node-badge-light-color-palette.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
@@ -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'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 108 KiB |
@@ -1,8 +1,17 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import fs from 'fs'
|
||||
import { Page, expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
async function checkTemplateFileExists(
|
||||
page: Page,
|
||||
filename: string
|
||||
): Promise<boolean> {
|
||||
const response = await page.request.head(
|
||||
new URL(`/templates/${filename}`, page.url()).toString()
|
||||
)
|
||||
return response.ok()
|
||||
}
|
||||
|
||||
test.describe('Templates', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
@@ -12,32 +21,32 @@ test.describe('Templates', () => {
|
||||
test('should have a JSON workflow file for each template', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
test.slow()
|
||||
const templates = await comfyPage.templates.getAllTemplates()
|
||||
for (const template of templates) {
|
||||
const workflowPath = comfyPage.templates.getTemplatePath(
|
||||
const exists = await checkTemplateFileExists(
|
||||
comfyPage.page,
|
||||
`${template.name}.json`
|
||||
)
|
||||
expect(
|
||||
fs.existsSync(workflowPath),
|
||||
`Missing workflow: ${template.name}`
|
||||
).toBe(true)
|
||||
expect(exists, `Missing workflow: ${template.name}`).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
test('should have all required thumbnail media for each template', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
test.slow()
|
||||
const templates = await comfyPage.templates.getAllTemplates()
|
||||
for (const template of templates) {
|
||||
const { name, mediaSubtype, thumbnailVariant } = template
|
||||
const baseMedia = `${name}-1.${mediaSubtype}`
|
||||
const basePath = comfyPage.templates.getTemplatePath(baseMedia)
|
||||
|
||||
// Check base thumbnail
|
||||
expect(
|
||||
fs.existsSync(basePath),
|
||||
`Missing base thumbnail: ${baseMedia}`
|
||||
).toBe(true)
|
||||
const baseExists = await checkTemplateFileExists(
|
||||
comfyPage.page,
|
||||
baseMedia
|
||||
)
|
||||
expect(baseExists, `Missing base thumbnail: ${baseMedia}`).toBe(true)
|
||||
|
||||
// Check second thumbnail for variants that need it
|
||||
if (
|
||||
@@ -45,9 +54,12 @@ test.describe('Templates', () => {
|
||||
thumbnailVariant === 'hoverDissolve'
|
||||
) {
|
||||
const secondMedia = `${name}-2.${mediaSubtype}`
|
||||
const secondPath = comfyPage.templates.getTemplatePath(secondMedia)
|
||||
const secondExists = await checkTemplateFileExists(
|
||||
comfyPage.page,
|
||||
secondMedia
|
||||
)
|
||||
expect(
|
||||
fs.existsSync(secondPath),
|
||||
secondExists,
|
||||
`Missing second thumbnail: ${secondMedia} required for ${thumbnailVariant}`
|
||||
).toBe(true)
|
||||
}
|
||||
@@ -86,4 +98,48 @@ test.describe('Templates', () => {
|
||||
// Expect the templates dialog to be shown
|
||||
expect(await comfyPage.templates.content.isVisible()).toBe(true)
|
||||
})
|
||||
|
||||
test('Uses title field as fallback when the key is not found in locales', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Capture request for the index.json
|
||||
await comfyPage.page.route('**/templates/index.json', async (route, _) => {
|
||||
// Add a new template that won't have a translation pre-generated
|
||||
const response = [
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'FALLBACK CATEGORY',
|
||||
type: 'image',
|
||||
templates: [
|
||||
{
|
||||
name: 'unknown_key_has_no_translation_available',
|
||||
title: 'FALLBACK TEMPLATE NAME',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'No translations found'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify(response),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Load the templates dialog
|
||||
await comfyPage.executeCommand('Comfy.BrowseTemplates')
|
||||
|
||||
// Expect the title to be used as fallback for template cards
|
||||
await expect(
|
||||
comfyPage.templates.content.getByText('FALLBACK TEMPLATE NAME')
|
||||
).toBeVisible()
|
||||
|
||||
// Expect the title to be used as fallback for the template categories
|
||||
await expect(comfyPage.page.getByLabel('FALLBACK CATEGORY')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -186,6 +186,105 @@ test.describe('Image widget', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Animated image widget', () => {
|
||||
test('Shows preview of uploaded animated image', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('widgets/load_animated_webp')
|
||||
|
||||
// Get position of the load animated webp node
|
||||
const nodes = await comfyPage.getNodeRefsByType(
|
||||
'DevToolsLoadAnimatedImageTest'
|
||||
)
|
||||
const loadAnimatedWebpNode = nodes[0]
|
||||
const { x, y } = await loadAnimatedWebpNode.getPosition()
|
||||
|
||||
// Drag and drop image file onto the load animated webp node
|
||||
await comfyPage.dragAndDropFile('animated_webp.webp', {
|
||||
dropPosition: { x, y }
|
||||
})
|
||||
|
||||
// Expect the image preview to change automatically
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'animated_image_preview_drag_and_dropped.png'
|
||||
)
|
||||
|
||||
// Wait for animation to go to next frame
|
||||
await comfyPage.page.waitForTimeout(512)
|
||||
|
||||
// Move mouse and click on canvas to trigger render
|
||||
await comfyPage.page.mouse.click(64, 64)
|
||||
|
||||
// Expect the image preview to change to the next frame of the animation
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'animated_image_preview_drag_and_dropped_next_frame.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can drag-and-drop animated webp image', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('widgets/load_animated_webp')
|
||||
|
||||
// Get position of the load animated webp node
|
||||
const nodes = await comfyPage.getNodeRefsByType(
|
||||
'DevToolsLoadAnimatedImageTest'
|
||||
)
|
||||
const loadAnimatedWebpNode = nodes[0]
|
||||
const { x, y } = await loadAnimatedWebpNode.getPosition()
|
||||
|
||||
// Drag and drop image file onto the load animated webp node
|
||||
await comfyPage.dragAndDropFile('animated_webp.webp', {
|
||||
dropPosition: { x, y }
|
||||
})
|
||||
|
||||
// Expect the filename combo value to be updated
|
||||
const fileComboWidget = await loadAnimatedWebpNode.getWidget(0)
|
||||
const filename = await fileComboWidget.getValue()
|
||||
expect(filename).toContain('animated_webp.webp')
|
||||
})
|
||||
|
||||
test('Can preview saved animated webp image', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('widgets/save_animated_webp')
|
||||
|
||||
// Get position of the load animated webp node
|
||||
const loadNodes = await comfyPage.getNodeRefsByType(
|
||||
'DevToolsLoadAnimatedImageTest'
|
||||
)
|
||||
const loadAnimatedWebpNode = loadNodes[0]
|
||||
const { x, y } = await loadAnimatedWebpNode.getPosition()
|
||||
|
||||
// Drag and drop image file onto the load animated webp node
|
||||
await comfyPage.dragAndDropFile('animated_webp.webp', {
|
||||
dropPosition: { x, y }
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Get the SaveAnimatedWEBP node
|
||||
const saveNodes = await comfyPage.getNodeRefsByType('SaveAnimatedWEBP')
|
||||
const saveAnimatedWebpNode = saveNodes[0]
|
||||
if (!saveAnimatedWebpNode)
|
||||
throw new Error('SaveAnimatedWEBP node not found')
|
||||
|
||||
// Simulate the graph executing
|
||||
await comfyPage.page.evaluate(
|
||||
([loadId, saveId]) => {
|
||||
// Set the output of the SaveAnimatedWEBP node to equal the loader node's image
|
||||
window['app'].nodeOutputs[saveId] = window['app'].nodeOutputs[loadId]
|
||||
},
|
||||
[loadAnimatedWebpNode.id, saveAnimatedWebpNode.id]
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Wait for animation to go to next frame
|
||||
await comfyPage.page.waitForTimeout(512)
|
||||
|
||||
// Move mouse and click on canvas to trigger render
|
||||
await comfyPage.page.mouse.click(64, 64)
|
||||
|
||||
// Expect the SaveAnimatedWEBP node to have an output preview
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'animated_image_preview_saved_webp.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Load audio widget', () => {
|
||||
test('Can load audio', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('widgets/load_audio_widget')
|
||||
|
||||
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 113 KiB |
59
build/plugins/addElementVnodeExportPlugin.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Plugin } from 'vite'
|
||||
|
||||
/**
|
||||
* Vite plugin that adds an alias export for Vue's createBaseVNode as createElementVNode.
|
||||
*
|
||||
* This plugin addresses compatibility issues where some components or libraries
|
||||
* might be using the older createElementVNode function name instead of createBaseVNode.
|
||||
* It modifies the Vue vendor chunk during build to add the alias export.
|
||||
*
|
||||
* @returns {Plugin} A Vite plugin that modifies the Vue vendor chunk exports
|
||||
*/
|
||||
export function addElementVnodeExportPlugin(): Plugin {
|
||||
return {
|
||||
name: 'add-element-vnode-export-plugin',
|
||||
|
||||
renderChunk(code, chunk, _options) {
|
||||
if (chunk.name.startsWith('vendor-vue')) {
|
||||
const exportRegex = /(export\s*\{)([^}]*)(\}\s*;?\s*)$/
|
||||
const match = code.match(exportRegex)
|
||||
|
||||
if (match) {
|
||||
const existingExports = match[2].trim()
|
||||
const exportsArray = existingExports
|
||||
.split(',')
|
||||
.map((e) => e.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
const hasCreateBaseVNode = exportsArray.some((e) =>
|
||||
e.startsWith('createBaseVNode')
|
||||
)
|
||||
const hasCreateElementVNode = exportsArray.some((e) =>
|
||||
e.includes('createElementVNode')
|
||||
)
|
||||
|
||||
if (hasCreateBaseVNode && !hasCreateElementVNode) {
|
||||
const newExportStatement = `${match[1]} ${existingExports ? existingExports + ',' : ''} createBaseVNode as createElementVNode ${match[3]}`
|
||||
const newCode = code.replace(exportRegex, newExportStatement)
|
||||
|
||||
console.log(
|
||||
`[add-element-vnode-export-plugin] Added 'createBaseVNode as createElementVNode' export to vendor-vue chunk.`
|
||||
)
|
||||
|
||||
return { code: newCode, map: null }
|
||||
} else if (!hasCreateBaseVNode) {
|
||||
console.warn(
|
||||
`[add-element-vnode-export-plugin] Warning: 'createBaseVNode' not found in exports of vendor-vue chunk. Cannot add alias.`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
`[add-element-vnode-export-plugin] Warning: Could not find expected export block format in vendor-vue chunk.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
82
build/plugins/comfyAPIPlugin.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import path from 'path'
|
||||
import { Plugin } from 'vite'
|
||||
|
||||
interface ShimResult {
|
||||
code: string
|
||||
exports: string[]
|
||||
}
|
||||
|
||||
function isLegacyFile(id: string): boolean {
|
||||
return (
|
||||
id.endsWith('.ts') &&
|
||||
(id.includes('src/extensions/core') || id.includes('src/scripts'))
|
||||
)
|
||||
}
|
||||
|
||||
function transformExports(code: string, id: string): ShimResult {
|
||||
const moduleName = getModuleName(id)
|
||||
const exports: string[] = []
|
||||
let newCode = code
|
||||
|
||||
// Regex to match different types of exports
|
||||
const regex =
|
||||
/export\s+(const|let|var|function|class|async function)\s+([a-zA-Z$_][a-zA-Z\d$_]*)(\s|\()/g
|
||||
let match
|
||||
|
||||
while ((match = regex.exec(code)) !== null) {
|
||||
const name = match[2]
|
||||
// All exports should be bind to the window object as new API endpoint.
|
||||
if (exports.length == 0) {
|
||||
newCode += `\nwindow.comfyAPI = window.comfyAPI || {};`
|
||||
newCode += `\nwindow.comfyAPI.${moduleName} = window.comfyAPI.${moduleName} || {};`
|
||||
}
|
||||
newCode += `\nwindow.comfyAPI.${moduleName}.${name} = ${name};`
|
||||
exports.push(
|
||||
`export const ${name} = window.comfyAPI.${moduleName}.${name};\n`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
code: newCode,
|
||||
exports
|
||||
}
|
||||
}
|
||||
|
||||
function getModuleName(id: string): string {
|
||||
// Simple example to derive a module name from the file path
|
||||
const parts = id.split('/')
|
||||
const fileName = parts[parts.length - 1]
|
||||
return fileName.replace(/\.\w+$/, '') // Remove file extension
|
||||
}
|
||||
|
||||
export function comfyAPIPlugin(isDev: boolean): Plugin {
|
||||
return {
|
||||
name: 'comfy-api-plugin',
|
||||
transform(code: string, id: string) {
|
||||
if (isDev) return null
|
||||
|
||||
if (isLegacyFile(id)) {
|
||||
const result = transformExports(code, id)
|
||||
|
||||
if (result.exports.length > 0) {
|
||||
const projectRoot = process.cwd()
|
||||
const relativePath = path.relative(path.join(projectRoot, 'src'), id)
|
||||
const shimFileName = relativePath.replace(/\.ts$/, '.js')
|
||||
|
||||
const shimComment = `// Shim for ${relativePath}\n`
|
||||
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: shimFileName,
|
||||
source: shimComment + result.exports.join('')
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
code: result.code,
|
||||
map: null // If you're not modifying the source map, return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
103
build/plugins/generateImportMapPlugin.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { OutputOptions } from 'rollup'
|
||||
import { HtmlTagDescriptor, Plugin } from 'vite'
|
||||
|
||||
interface VendorLibrary {
|
||||
name: string
|
||||
pattern: RegExp
|
||||
}
|
||||
|
||||
/**
|
||||
* Vite plugin that generates an import map for vendor chunks.
|
||||
*
|
||||
* This plugin creates a browser-compatible import map that maps module specifiers
|
||||
* (like 'vue' or 'primevue') to their actual file locations in the build output.
|
||||
* This improves module loading in modern browsers and enables better caching.
|
||||
*
|
||||
* The plugin:
|
||||
* 1. Tracks vendor chunks during bundle generation
|
||||
* 2. Creates mappings between module names and their file paths
|
||||
* 3. Injects an import map script tag into the HTML head
|
||||
* 4. Configures manual chunk splitting for vendor libraries
|
||||
*
|
||||
* @param vendorLibraries - An array of vendor libraries to split into separate chunks
|
||||
* @returns {Plugin} A Vite plugin that generates and injects an import map
|
||||
*/
|
||||
export function generateImportMapPlugin(
|
||||
vendorLibraries: VendorLibrary[]
|
||||
): Plugin {
|
||||
const importMapEntries: Record<string, string> = {}
|
||||
|
||||
return {
|
||||
name: 'generate-import-map-plugin',
|
||||
|
||||
// Configure manual chunks during the build process
|
||||
configResolved(config) {
|
||||
if (config.build) {
|
||||
// Ensure rollupOptions exists
|
||||
if (!config.build.rollupOptions) {
|
||||
config.build.rollupOptions = {}
|
||||
}
|
||||
|
||||
const outputOptions: OutputOptions = {
|
||||
manualChunks: (id: string) => {
|
||||
for (const lib of vendorLibraries) {
|
||||
if (lib.pattern.test(id)) {
|
||||
return `vendor-${lib.name}`
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
// Disable minification of internal exports to preserve function names
|
||||
minifyInternalExports: false
|
||||
}
|
||||
config.build.rollupOptions.output = outputOptions
|
||||
}
|
||||
},
|
||||
|
||||
generateBundle(_options, bundle) {
|
||||
for (const fileName in bundle) {
|
||||
const chunk = bundle[fileName]
|
||||
if (chunk.type === 'chunk' && !chunk.isEntry) {
|
||||
// Find matching vendor library by chunk name
|
||||
const vendorLib = vendorLibraries.find(
|
||||
(lib) => chunk.name === `vendor-${lib.name}`
|
||||
)
|
||||
|
||||
if (vendorLib) {
|
||||
const relativePath = `./${chunk.fileName.replace(/\\/g, '/')}`
|
||||
importMapEntries[vendorLib.name] = relativePath
|
||||
|
||||
console.log(
|
||||
`[ImportMap Plugin] Found chunk: ${chunk.name} -> Mapped '${vendorLib.name}' to '${relativePath}'`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
transformIndexHtml(html) {
|
||||
if (Object.keys(importMapEntries).length === 0) {
|
||||
console.warn(
|
||||
'[ImportMap Plugin] No vendor chunks found to create import map.'
|
||||
)
|
||||
return html
|
||||
}
|
||||
|
||||
const importMap = {
|
||||
imports: importMapEntries
|
||||
}
|
||||
|
||||
const importMapTag: HtmlTagDescriptor = {
|
||||
tag: 'script',
|
||||
attrs: { type: 'importmap' },
|
||||
children: JSON.stringify(importMap, null, 2),
|
||||
injectTo: 'head'
|
||||
}
|
||||
|
||||
return {
|
||||
html,
|
||||
tags: [importMapTag]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
build/plugins/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { addElementVnodeExportPlugin } from './addElementVnodeExportPlugin'
|
||||
export { comfyAPIPlugin } from './comfyAPIPlugin'
|
||||
export { generateImportMapPlugin } from './generateImportMapPlugin'
|
||||
1
global.d.ts
vendored
@@ -3,6 +3,7 @@ declare const __SENTRY_ENABLED__: boolean
|
||||
declare const __SENTRY_DSN__: string
|
||||
declare const __ALGOLIA_APP_ID__: string
|
||||
declare const __ALGOLIA_API_KEY__: string
|
||||
declare const __USE_PROD_CONFIG__: boolean
|
||||
|
||||
interface Navigator {
|
||||
/**
|
||||
|
||||
20
package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import fs from 'fs-extra'
|
||||
import { execSync } from 'node:child_process'
|
||||
import path from 'node:path'
|
||||
|
||||
const workflowTemplatesRepo = 'https://github.com/Comfy-Org/workflow_templates'
|
||||
const tempRepoDir = './templates_repo'
|
||||
|
||||
// Clone the repository
|
||||
execSync(`git clone ${workflowTemplatesRepo} --depth 1 ${tempRepoDir}`)
|
||||
|
||||
// Create public/templates directory if it doesn't exist
|
||||
fs.ensureDirSync('public/templates')
|
||||
|
||||
// Copy templates from repo to public/templates
|
||||
const sourceDir = path.join(tempRepoDir, 'templates')
|
||||
const targetDir = 'public/templates'
|
||||
|
||||
// Copy entire directory at once
|
||||
fs.copySync(sourceDir, targetDir)
|
||||
|
||||
// Remove the temporary repository directory
|
||||
fs.removeSync(tempRepoDir)
|
||||
|
||||
console.log('Templates fetched successfully')
|
||||
31
src/components/common/ApiNodesList.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3 h-full">
|
||||
<div class="flex text-xs">
|
||||
<div>{{ t('apiNodesCostBreakdown.title') }}</div>
|
||||
</div>
|
||||
<ScrollPanel class="flex-grow h-0">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
v-for="nodeName in nodeNames"
|
||||
:key="nodeName"
|
||||
class="flex items-center justify-between px-3 py-2 rounded-md bg-[var(--p-content-border-color)]"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-base font-medium leading-tight">{{
|
||||
nodeName
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { nodeNames } = defineProps<{ nodeNames: string[] }>()
|
||||
</script>
|
||||
@@ -9,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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
161
src/components/dialog/content/TopUpCreditsDialogContent.vue
Normal 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>
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<em v-else-if="segment.type === 'italic'">{{ segment.text }}</em>
|
||||
<code
|
||||
v-else-if="segment.type === 'code'"
|
||||
class="bg-surface-100 px-1 py-0.5 rounded text-xs"
|
||||
class="px-1 py-0.5 rounded text-xs"
|
||||
>{{ segment.text }}</code
|
||||
>
|
||||
<span v-else>{{ segment.text }}</span>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div
|
||||
v-for="nodeDef in mappedNodeDefs"
|
||||
:key="createNodeDefKey(nodeDef)"
|
||||
class="border border-surface-border rounded-lg p-4"
|
||||
class="border rounded-lg p-4"
|
||||
>
|
||||
<NodePreview :node-def="nodeDef" class="!text-[.625rem] !min-w-full" />
|
||||
</div>
|
||||
|
||||
@@ -10,9 +10,7 @@
|
||||
zIndex: maxVisible - index
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="border border-surface-border bg-surface-card rounded-lg p-0.5"
|
||||
>
|
||||
<div class="border rounded-lg p-0.5">
|
||||
<PackIcon :node-pack="pack" width="4.5rem" height="4.5rem" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
187
src/components/dialog/content/setting/CreditsPanel.vue
Normal 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>
|
||||
137
src/components/dialog/content/setting/UserPanel.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -73,6 +73,7 @@ watch(
|
||||
updateWidgets()
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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` }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -37,7 +37,8 @@
|
||||
import {
|
||||
CUDA_TORCH_URL,
|
||||
NIGHTLY_CPU_TORCH_URL,
|
||||
TorchDeviceType
|
||||
TorchDeviceType,
|
||||
TorchMirrorUrl
|
||||
} from '@comfyorg/comfyui-electron-types'
|
||||
import Divider from 'primevue/divider'
|
||||
import Panel from 'primevue/panel'
|
||||
@@ -46,6 +47,7 @@ import { ModelRef, computed, onMounted, ref } from 'vue'
|
||||
import MirrorItem from '@/components/install/mirror/MirrorItem.vue'
|
||||
import { PYPI_MIRROR, PYTHON_MIRROR, UVMirror } from '@/constants/uvMirrors'
|
||||
import { t } from '@/i18n'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import { isInChina } from '@/utils/networkUtil'
|
||||
import { ValidationState, mergeValidationStates } from '@/utils/validationUtil'
|
||||
|
||||
@@ -55,6 +57,17 @@ const pythonMirror = defineModel<string>('pythonMirror', { required: true })
|
||||
const pypiMirror = defineModel<string>('pypiMirror', { required: true })
|
||||
const torchMirror = defineModel<string>('torchMirror', { required: true })
|
||||
|
||||
const isBlackwellArchitecture = ref(false)
|
||||
|
||||
const requiresNightlyPytorch = async (): Promise<boolean> => {
|
||||
try {
|
||||
return await electronAPI().isBlackwell()
|
||||
} catch (error) {
|
||||
console.error('Failed to detect Blackwell architecture:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
|
||||
const settingId = 'Comfy-Desktop.UV.TorchInstallMirror'
|
||||
switch (device) {
|
||||
@@ -65,6 +78,13 @@ const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
|
||||
fallbackMirror: NIGHTLY_CPU_TORCH_URL
|
||||
}
|
||||
case 'nvidia':
|
||||
if (isBlackwellArchitecture.value) {
|
||||
return {
|
||||
settingId,
|
||||
mirror: TorchMirrorUrl.NightlyCuda,
|
||||
fallbackMirror: TorchMirrorUrl.NightlyCuda
|
||||
}
|
||||
}
|
||||
return {
|
||||
settingId,
|
||||
mirror: CUDA_TORCH_URL,
|
||||
@@ -83,6 +103,7 @@ const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
|
||||
const userIsInChina = ref(false)
|
||||
onMounted(async () => {
|
||||
userIsInChina.value = await isInChina()
|
||||
isBlackwellArchitecture.value = await requiresNightlyPytorch()
|
||||
})
|
||||
|
||||
const useFallbackMirror = (mirror: UVMirror) => ({
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
<Slider
|
||||
v-model="lightIntensity"
|
||||
class="w-full"
|
||||
:min="1"
|
||||
:max="20"
|
||||
:step="1"
|
||||
:min="lightIntensityMinimum"
|
||||
:max="lightIntensityMaximum"
|
||||
:step="lightAdjustmentIncrement"
|
||||
@change="updateLightIntensity"
|
||||
/>
|
||||
</div>
|
||||
@@ -38,6 +38,7 @@ import Slider from 'primevue/slider'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const vTooltip = Tooltip
|
||||
|
||||
@@ -54,6 +55,16 @@ const lightIntensity = ref(props.lightIntensity)
|
||||
const showLightIntensityButton = ref(props.showLightIntensityButton)
|
||||
const showLightIntensity = ref(false)
|
||||
|
||||
const lightIntensityMaximum = useSettingStore().get(
|
||||
'Comfy.Load3D.LightIntensityMaximum'
|
||||
)
|
||||
const lightIntensityMinimum = useSettingStore().get(
|
||||
'Comfy.Load3D.LightIntensityMinimum'
|
||||
)
|
||||
const lightAdjustmentIncrement = useSettingStore().get(
|
||||
'Comfy.Load3D.LightAdjustmentIncrement'
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.lightIntensity,
|
||||
(newValue) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -52,21 +52,15 @@
|
||||
<template #content>
|
||||
<div class="flex items-center px-4 py-3">
|
||||
<div class="flex-1">
|
||||
<h3
|
||||
class="line-clamp-1 text-lg font-normal text-surface-900 dark:text-surface-100"
|
||||
:title="title"
|
||||
>
|
||||
<h3 class="line-clamp-1 text-lg font-normal" :title="title">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<p
|
||||
class="line-clamp-2 text-sm text-surface-600 dark:text text-muted"
|
||||
:title="description"
|
||||
>
|
||||
<p class="line-clamp-2 text-sm text-muted" :title="description">
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="flex md:hidden xl:flex items-center justify-center ml-4 w-10 h-10 rounded-full bg-surface-100"
|
||||
class="flex md:hidden xl:flex items-center justify-center ml-4 w-10 h-10 rounded-full"
|
||||
>
|
||||
<i class="pi pi-angle-right text-2xl" />
|
||||
</div>
|
||||
@@ -123,12 +117,13 @@ const overlayThumbnailSrc = computed(() =>
|
||||
)
|
||||
|
||||
const title = computed(() => {
|
||||
const fallback = template.title ?? template.name ?? `${sourceModule} Template`
|
||||
return sourceModule === 'default'
|
||||
? st(
|
||||
`templateWorkflows.template.${normalizeI18nKey(categoryTitle)}.${normalizeI18nKey(template.name)}`,
|
||||
template.name
|
||||
fallback
|
||||
)
|
||||
: template.name ?? `${sourceModule} Template`
|
||||
: fallback
|
||||
})
|
||||
|
||||
const description = computed(() => template.description.replace(/[-_]/g, ' '))
|
||||
|
||||
@@ -10,11 +10,8 @@
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="w-full h-full flex items-center justify-center bg-surface-card"
|
||||
>
|
||||
<i class="pi pi-file text-4xl text-surface-600" />
|
||||
<div v-else class="w-full h-full flex items-center justify-center">
|
||||
<i class="pi pi-file text-4xl" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
49
src/components/topbar/CurrentUserButton.vue
Normal 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>
|
||||
@@ -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')
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ export const useCanvasPositionConversion = (
|
||||
const clientPosToCanvasPos = (pos: Vector2): Vector2 => {
|
||||
const { offset, scale } = lgCanvas.ds
|
||||
return [
|
||||
(pos[0] - left.value) / scale + offset[0],
|
||||
(pos[1] - top.value) / scale + offset[1]
|
||||
(pos[0] - left.value) / scale - offset[0],
|
||||
(pos[1] - top.value) / scale - offset[1]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { IWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
import { ANIM_PREVIEW_WIDGET } from '@/scripts/app'
|
||||
import { createImageHost } from '@/scripts/ui/imagePreview'
|
||||
import { fitDimensionsToNodeWidth } from '@/utils/imageUtil'
|
||||
|
||||
/**
|
||||
* Composable for handling animated image previews in nodes
|
||||
@@ -42,6 +43,16 @@ export function useNodeAnimatedImage() {
|
||||
widget.serialize = false
|
||||
widget.serializeValue = () => undefined
|
||||
widget.options.host.updateImages(node.imgs)
|
||||
widget.computeLayoutSize = () => {
|
||||
const img = widget.options.host.getCurrentImage()
|
||||
if (!img) return { minHeight: 0, minWidth: 0 }
|
||||
|
||||
return fitDimensionsToNodeWidth(
|
||||
img.naturalWidth,
|
||||
img.naturalHeight,
|
||||
node.size?.[0] || 0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { fitDimensionsToNodeWidth } from '@/utils/imageUtil'
|
||||
|
||||
const VIDEO_WIDGET_NAME = 'video-preview'
|
||||
const VIDEO_DEFAULT_OPTIONS = {
|
||||
@@ -131,12 +132,15 @@ export const useNodeVideo = (node: LGraphNode) => {
|
||||
let minWidth = DEFAULT_VIDEO_SIZE
|
||||
|
||||
const setMinDimensions = (video: HTMLVideoElement) => {
|
||||
const intrinsicAspectRatio = video.videoWidth / video.videoHeight
|
||||
if (!intrinsicAspectRatio || isNaN(intrinsicAspectRatio)) return
|
||||
const { minHeight: calculatedHeight, minWidth: calculatedWidth } =
|
||||
fitDimensionsToNodeWidth(
|
||||
video.videoWidth,
|
||||
video.videoHeight,
|
||||
node.size?.[0] || DEFAULT_VIDEO_SIZE
|
||||
)
|
||||
|
||||
// Set min. height s.t. video spans node's x-axis while maintaining aspect ratio
|
||||
minWidth = node.size?.[0] || DEFAULT_VIDEO_SIZE
|
||||
minHeight = Math.max(minWidth / intrinsicAspectRatio, 64)
|
||||
minWidth = calculatedWidth
|
||||
minHeight = calculatedHeight
|
||||
}
|
||||
|
||||
const loadElement = (url: string): Promise<HTMLVideoElement | null> =>
|
||||
|
||||
@@ -5,6 +5,8 @@ import { useNodePacks } from '@/composables/nodePack/useNodePacks'
|
||||
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import { SelectedVersion, UseNodePacksOptions } from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
@@ -22,6 +24,8 @@ const CORE_NODES_PACK_NAME = 'comfy-core'
|
||||
* associated node packs from the registry
|
||||
*/
|
||||
export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
const { search } = useComfyRegistryStore()
|
||||
|
||||
const workflowPacks = ref<WorkflowPack[]>([])
|
||||
@@ -36,6 +40,13 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean the version string to be used in the registry search.
|
||||
* Removes the leading 'v' and trims whitespace and line terminators.
|
||||
*/
|
||||
const cleanVersionString = (version: string) =>
|
||||
version.replace(/^v/, '').trim()
|
||||
|
||||
/**
|
||||
* Infer the pack for a node by searching the registry for packs that have nodes
|
||||
* with the same name.
|
||||
@@ -44,6 +55,22 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
|
||||
node: LGraphNode
|
||||
): Promise<WorkflowPack | undefined> => {
|
||||
const nodeName = node.type
|
||||
|
||||
// Check if node is a core node
|
||||
const nodeDef = nodeDefStore.nodeDefsByName[nodeName]
|
||||
if (nodeDef?.nodeSource.type === 'core') {
|
||||
if (!systemStatsStore.systemStats) {
|
||||
await systemStatsStore.fetchSystemStats()
|
||||
}
|
||||
return {
|
||||
id: CORE_NODES_PACK_NAME,
|
||||
version:
|
||||
systemStatsStore.systemStats?.system?.comfyui_version ??
|
||||
SelectedVersion.NIGHTLY
|
||||
}
|
||||
}
|
||||
|
||||
// Search the registry for non-core nodes
|
||||
const searchResult = await search.call({
|
||||
comfy_node_search: nodeName,
|
||||
limit: 1
|
||||
@@ -70,7 +97,9 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
|
||||
if (packId === CORE_NODES_PACK_NAME) return undefined
|
||||
|
||||
const version =
|
||||
typeof node.properties.ver === 'string' ? node.properties.ver : undefined
|
||||
typeof node.properties.ver === 'string'
|
||||
? cleanVersionString(node.properties.ver)
|
||||
: undefined
|
||||
|
||||
return {
|
||||
id: packId,
|
||||
|
||||
122
src/composables/setting/useSettingSearch.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { st } from '@/i18n'
|
||||
import {
|
||||
SettingTreeNode,
|
||||
getSettingInfo,
|
||||
useSettingStore
|
||||
} from '@/stores/settingStore'
|
||||
import { ISettingGroup, SettingParams } from '@/types/settingTypes'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
export function useSettingSearch() {
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const searchQuery = ref<string>('')
|
||||
const filteredSettingIds = ref<string[]>([])
|
||||
const searchInProgress = ref<boolean>(false)
|
||||
|
||||
watch(searchQuery, () => (searchInProgress.value = true))
|
||||
|
||||
/**
|
||||
* Settings categories that contains at least one setting in search results.
|
||||
*/
|
||||
const searchResultsCategories = computed<Set<string>>(() => {
|
||||
return new Set(
|
||||
filteredSettingIds.value.map(
|
||||
(id) => getSettingInfo(settingStore.settingsById[id]).category
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Check if the search query is empty
|
||||
*/
|
||||
const queryIsEmpty = computed(() => searchQuery.value.length === 0)
|
||||
|
||||
/**
|
||||
* Check if we're in search mode
|
||||
*/
|
||||
const inSearch = computed(
|
||||
() => !queryIsEmpty.value && !searchInProgress.value
|
||||
)
|
||||
|
||||
/**
|
||||
* Handle search functionality
|
||||
*/
|
||||
const handleSearch = (query: string) => {
|
||||
if (!query) {
|
||||
filteredSettingIds.value = []
|
||||
return
|
||||
}
|
||||
|
||||
const queryLower = query.toLocaleLowerCase()
|
||||
const allSettings = Object.values(settingStore.settingsById)
|
||||
const filteredSettings = allSettings.filter((setting) => {
|
||||
const idLower = setting.id.toLowerCase()
|
||||
const nameLower = setting.name.toLowerCase()
|
||||
const translatedName = st(
|
||||
`settings.${normalizeI18nKey(setting.id)}.name`,
|
||||
setting.name
|
||||
).toLocaleLowerCase()
|
||||
const info = getSettingInfo(setting)
|
||||
const translatedCategory = st(
|
||||
`settingsCategories.${normalizeI18nKey(info.category)}`,
|
||||
info.category
|
||||
).toLocaleLowerCase()
|
||||
const translatedSubCategory = st(
|
||||
`settingsCategories.${normalizeI18nKey(info.subCategory)}`,
|
||||
info.subCategory
|
||||
).toLocaleLowerCase()
|
||||
|
||||
return (
|
||||
idLower.includes(queryLower) ||
|
||||
nameLower.includes(queryLower) ||
|
||||
translatedName.includes(queryLower) ||
|
||||
translatedCategory.includes(queryLower) ||
|
||||
translatedSubCategory.includes(queryLower)
|
||||
)
|
||||
})
|
||||
|
||||
filteredSettingIds.value = filteredSettings.map((x) => x.id)
|
||||
searchInProgress.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get search results grouped by category
|
||||
*/
|
||||
const getSearchResults = (
|
||||
activeCategory: SettingTreeNode | null
|
||||
): ISettingGroup[] => {
|
||||
const groupedSettings: { [key: string]: SettingParams[] } = {}
|
||||
|
||||
filteredSettingIds.value.forEach((id) => {
|
||||
const setting = settingStore.settingsById[id]
|
||||
const info = getSettingInfo(setting)
|
||||
const groupLabel = info.subCategory
|
||||
|
||||
if (activeCategory === null || activeCategory.label === info.category) {
|
||||
if (!groupedSettings[groupLabel]) {
|
||||
groupedSettings[groupLabel] = []
|
||||
}
|
||||
groupedSettings[groupLabel].push(setting)
|
||||
}
|
||||
})
|
||||
|
||||
return Object.entries(groupedSettings).map(([label, settings]) => ({
|
||||
label,
|
||||
settings
|
||||
}))
|
||||
}
|
||||
|
||||
return {
|
||||
searchQuery,
|
||||
filteredSettingIds,
|
||||
searchInProgress,
|
||||
searchResultsCategories,
|
||||
queryIsEmpty,
|
||||
inSearch,
|
||||
handleSearch,
|
||||
getSearchResults
|
||||
}
|
||||
}
|
||||
155
src/composables/setting/useSettingUI.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
type ComfyWidgetConstructorV2,
|
||||
addValueControlWidgets
|
||||
} from '@/scripts/widgets'
|
||||
import { generateUUID } from '@/utils/formatUtil'
|
||||
|
||||
import { useRemoteWidget } from './useRemoteWidget'
|
||||
|
||||
@@ -32,7 +31,6 @@ const getDefaultValue = (inputSpec: ComboInputSpec) => {
|
||||
const addMultiSelectWidget = (node: LGraphNode, inputSpec: ComboInputSpec) => {
|
||||
const widgetValue = ref<string[]>([])
|
||||
const widget = new ComponentWidgetImpl({
|
||||
id: generateUUID(),
|
||||
node,
|
||||
name: inputSpec.name,
|
||||
component: MultiSelectWidget,
|
||||
|
||||
@@ -37,6 +37,7 @@ export const useImageUploadWidget = () => {
|
||||
const { imageInputName, allow_batch, image_folder = 'input' } = inputOptions
|
||||
const nodeOutputStore = useNodeOutputStore()
|
||||
|
||||
const isAnimated = !!inputOptions.animated_image_upload
|
||||
const isVideo = !!inputOptions.video_upload
|
||||
const accept = isVideo ? ACCEPTED_VIDEO_TYPES : ACCEPTED_IMAGE_TYPES
|
||||
const { showPreview } = isVideo ? useNodeVideo(node) : useNodeImage(node)
|
||||
@@ -92,7 +93,9 @@ export const useImageUploadWidget = () => {
|
||||
|
||||
// Add our own callback to the combo widget to render an image when it changes
|
||||
fileComboWidget.callback = function () {
|
||||
nodeOutputStore.setNodeOutputs(node, fileComboWidget.value)
|
||||
nodeOutputStore.setNodeOutputs(node, fileComboWidget.value, {
|
||||
isAnimated
|
||||
})
|
||||
node.graph?.setDirtyCanvas(true)
|
||||
}
|
||||
|
||||
@@ -100,7 +103,9 @@ export const useImageUploadWidget = () => {
|
||||
// The value isnt set immediately so we need to wait a moment
|
||||
// No change callbacks seem to be fired on initial setting of the value
|
||||
requestAnimationFrame(() => {
|
||||
nodeOutputStore.setNodeOutputs(node, fileComboWidget.value)
|
||||
nodeOutputStore.setNodeOutputs(node, fileComboWidget.value, {
|
||||
isAnimated
|
||||
})
|
||||
showPreview({ block: false })
|
||||
})
|
||||
|
||||
|
||||
3
src/config/comfyApi.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const COMFY_API_BASE_URL = __USE_PROD_CONFIG__
|
||||
? 'https://api.comfy.org'
|
||||
: 'https://stagingapi.comfy.org'
|
||||
@@ -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
|
||||
|
||||
@@ -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']
|
||||
]
|
||||
]
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -158,6 +158,27 @@ import { checkMirrorReachable } from '@/utils/networkUtil'
|
||||
window.open('https://comfyorg.notion.site/', '_blank')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy-Desktop.CheckForUpdates',
|
||||
label: 'Check for Updates',
|
||||
icon: 'pi pi-sync',
|
||||
async function() {
|
||||
const updateAvailable = await electronAPI.checkForUpdates({
|
||||
disableUpdateReadyAction: true
|
||||
})
|
||||
if (updateAvailable.isUpdateAvailable) {
|
||||
const version = updateAvailable.version
|
||||
const proceed = await useDialogService().confirm({
|
||||
title: t('desktopUpdate.updateFoundTitle', { version }),
|
||||
message: t('desktopUpdate.updateAvailableMessage'),
|
||||
type: 'default'
|
||||
})
|
||||
if (proceed) {
|
||||
electronAPI.restartAndInstall()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy-Desktop.Reinstall',
|
||||
label: 'Reinstall',
|
||||
@@ -223,7 +244,7 @@ import { checkMirrorReachable } from '@/utils/networkUtil'
|
||||
},
|
||||
{
|
||||
path: ['Help'],
|
||||
commands: ['Comfy-Desktop.Reinstall']
|
||||
commands: ['Comfy-Desktop.CheckForUpdates', 'Comfy-Desktop.Reinstall']
|
||||
}
|
||||
],
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { generateUUID } from '@/utils/formatUtil'
|
||||
|
||||
useExtensionService().registerExtension({
|
||||
name: 'Comfy.Load3D',
|
||||
@@ -39,6 +38,16 @@ useExtensionService().registerExtension({
|
||||
defaultValue: true,
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Load3D.BackgroundColor',
|
||||
category: ['3D', 'Scene', 'Initial Background Color'],
|
||||
name: 'Initial Background Color',
|
||||
tooltip:
|
||||
'Controls the default background color of the 3D scene. This setting determines the background appearance when a new 3D widget is created, but can be adjusted individually for each widget after creation.',
|
||||
type: 'color',
|
||||
defaultValue: '282828',
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Load3D.CameraType',
|
||||
category: ['3D', 'Camera', 'Initial Camera Type'],
|
||||
@@ -49,6 +58,51 @@ useExtensionService().registerExtension({
|
||||
options: ['perspective', 'orthographic'],
|
||||
defaultValue: 'perspective',
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Load3D.LightIntensity',
|
||||
category: ['3D', 'Light', 'Initial Light Intensity'],
|
||||
name: 'Initial Light Intensity',
|
||||
tooltip:
|
||||
'Sets the default brightness level of lighting in the 3D scene. This value determines how intensely lights illuminate objects when a new 3D widget is created, but can be adjusted individually for each widget after creation.',
|
||||
type: 'number',
|
||||
defaultValue: 3,
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Load3D.LightIntensityMaximum',
|
||||
category: ['3D', 'Light', 'Light Intensity Maximum'],
|
||||
name: 'Light Intensity Maximum',
|
||||
tooltip:
|
||||
'Sets the maximum allowable light intensity value for 3D scenes. This defines the upper brightness limit that can be set when adjusting lighting in any 3D widget.',
|
||||
type: 'number',
|
||||
defaultValue: 10,
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Load3D.LightIntensityMinimum',
|
||||
category: ['3D', 'Light', 'Light Intensity Minimum'],
|
||||
name: 'Light Intensity Minimum',
|
||||
tooltip:
|
||||
'Sets the minimum allowable light intensity value for 3D scenes. This defines the lower brightness limit that can be set when adjusting lighting in any 3D widget.',
|
||||
type: 'number',
|
||||
defaultValue: 1,
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Load3D.LightAdjustmentIncrement',
|
||||
category: ['3D', 'Light', 'Light Adjustment Increment'],
|
||||
name: 'Light Adjustment Increment',
|
||||
tooltip:
|
||||
'Controls the increment size when adjusting light intensity in 3D scenes. A smaller step value allows for finer control over lighting adjustments, while a larger value results in more noticeable changes per adjustment.',
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 0.1,
|
||||
max: 1,
|
||||
step: 0.1
|
||||
},
|
||||
defaultValue: 0.5,
|
||||
experimental: true
|
||||
}
|
||||
],
|
||||
getCustomWidgets() {
|
||||
@@ -118,7 +172,6 @@ useExtensionService().registerExtension({
|
||||
}
|
||||
|
||||
const widget = new ComponentWidgetImpl({
|
||||
id: generateUUID(),
|
||||
node,
|
||||
name: inputSpec.name,
|
||||
component: Load3D,
|
||||
@@ -259,7 +312,6 @@ useExtensionService().registerExtension({
|
||||
}
|
||||
|
||||
const widget = new ComponentWidgetImpl({
|
||||
id: generateUUID(),
|
||||
node,
|
||||
name: inputSpec.name,
|
||||
component: Load3DAnimation,
|
||||
@@ -355,7 +407,6 @@ useExtensionService().registerExtension({
|
||||
}
|
||||
|
||||
const widget = new ComponentWidgetImpl({
|
||||
id: generateUUID(),
|
||||
node,
|
||||
name: inputSpec.name,
|
||||
component: Load3D,
|
||||
@@ -432,7 +483,6 @@ useExtensionService().registerExtension({
|
||||
}
|
||||
|
||||
const widget = new ComponentWidgetImpl({
|
||||
id: generateUUID(),
|
||||
node,
|
||||
name: inputSpec.name,
|
||||
component: Load3DAnimation,
|
||||
|
||||
@@ -91,12 +91,18 @@ class Load3DConfiguration {
|
||||
|
||||
this.load3d.togglePreview(showPreview)
|
||||
|
||||
const bgColor = this.load3d.loadNodeProperty('Background Color', '#282828')
|
||||
const bgColor = this.load3d.loadNodeProperty(
|
||||
'Background Color',
|
||||
'#' + useSettingStore().get('Comfy.Load3D.BackgroundColor')
|
||||
)
|
||||
|
||||
this.load3d.setBackgroundColor(bgColor)
|
||||
|
||||
const lightIntensity: number = Number(
|
||||
this.load3d.loadNodeProperty('Light Intensity', 5)
|
||||
this.load3d.loadNodeProperty(
|
||||
'Light Intensity',
|
||||
useSettingStore().get('Comfy.Load3D.LightIntensity')
|
||||
)
|
||||
)
|
||||
|
||||
this.load3d.setLightIntensity(lightIntensity)
|
||||
|
||||
@@ -2,16 +2,26 @@ import { applyTextReplacements } from '@/utils/searchAndReplace'
|
||||
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
const saveNodeTypes = new Set([
|
||||
'SaveImage',
|
||||
'SaveAnimatedWEBP',
|
||||
'SaveWEBM',
|
||||
'SaveAudio',
|
||||
'SaveGLB',
|
||||
'SaveAnimatedPNG',
|
||||
'CLIPSave',
|
||||
'VAESave',
|
||||
'ModelSave',
|
||||
'LoraSave',
|
||||
'SaveLatent'
|
||||
])
|
||||
|
||||
// Use widget values and dates in output filenames
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Comfy.SaveImageExtraOutput',
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (
|
||||
nodeData.name === 'SaveImage' ||
|
||||
nodeData.name === 'SaveAnimatedWEBP' ||
|
||||
nodeData.name === 'SaveWEBM'
|
||||
) {
|
||||
if (saveNodeTypes.has(nodeData.name)) {
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated
|
||||
// When the SaveImage node is created we want to override the serialization of the output name widget to run our S&R
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
import { generateUUID } from '@/utils/formatUtil'
|
||||
|
||||
useExtensionService().registerExtension({
|
||||
name: 'Comfy.SaveGLB',
|
||||
@@ -30,7 +29,6 @@ useExtensionService().registerExtension({
|
||||
}
|
||||
|
||||
const widget = new ComponentWidgetImpl({
|
||||
id: generateUUID(),
|
||||
node,
|
||||
name: inputSpec.name,
|
||||
component: Load3D,
|
||||
|
||||
@@ -14,7 +14,8 @@ const isMediaUploadComboInput = (inputSpec: InputSpec) => {
|
||||
|
||||
const isUploadInput =
|
||||
inputOptions['image_upload'] === true ||
|
||||
inputOptions['video_upload'] === true
|
||||
inputOptions['video_upload'] === true ||
|
||||
inputOptions['animated_image_upload'] === true
|
||||
|
||||
return (
|
||||
isUploadInput && (isComboInputSpecV1(inputSpec) || inputName === 'COMBO')
|
||||
|
||||
@@ -18,16 +18,6 @@ import { mergeInputSpec } from '@/utils/nodeDefUtil'
|
||||
import { applyTextReplacements } from '@/utils/searchAndReplace'
|
||||
import { isPrimitiveNode } from '@/utils/typeGuardUtil'
|
||||
|
||||
const VALID_TYPES = [
|
||||
'STRING',
|
||||
'combo',
|
||||
'number',
|
||||
'toggle',
|
||||
'BOOLEAN',
|
||||
'text',
|
||||
'string'
|
||||
]
|
||||
|
||||
const replacePropertyName = 'Run widget replace on values'
|
||||
export class PrimitiveNode extends LGraphNode {
|
||||
controlValues?: any[]
|
||||
@@ -47,7 +37,7 @@ export class PrimitiveNode extends LGraphNode {
|
||||
applyToGraph(extraLinks: LLink[] = []) {
|
||||
if (!this.outputs[0].links?.length) return
|
||||
|
||||
let links = [
|
||||
const links = [
|
||||
...this.outputs[0].links.map((l) => app.graph.links[l]),
|
||||
...extraLinks
|
||||
]
|
||||
@@ -58,30 +48,35 @@ export class PrimitiveNode extends LGraphNode {
|
||||
|
||||
// For each output link copy our value over the original widget value
|
||||
for (const linkInfo of links) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const node = this.graph.getNodeById(linkInfo.target_id)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const input = node.inputs[linkInfo.target_slot]
|
||||
let widget: IWidget | undefined
|
||||
const widgetName = (input.widget as { name: string }).name
|
||||
if (widgetName) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
widget = node.widgets.find((w) => w.name === widgetName)
|
||||
const node = this.graph?.getNodeById(linkInfo.target_id)
|
||||
const input = node?.inputs[linkInfo.target_slot]
|
||||
if (!input) {
|
||||
console.warn('Unable to resolve node or input for link', linkInfo)
|
||||
continue
|
||||
}
|
||||
|
||||
if (widget) {
|
||||
widget.value = v
|
||||
if (widget.callback) {
|
||||
widget.callback(
|
||||
widget.value,
|
||||
app.canvas,
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node,
|
||||
app.canvas.graph_mouse,
|
||||
{} as CanvasMouseEvent
|
||||
)
|
||||
}
|
||||
const widgetName = input.widget?.name
|
||||
if (!widgetName) {
|
||||
console.warn('Invalid widget or widget name', input.widget)
|
||||
continue
|
||||
}
|
||||
|
||||
const widget = node.widgets?.find((w) => w.name === widgetName)
|
||||
if (!widget) {
|
||||
console.warn(
|
||||
`Unable to find widget "${widgetName}" on node [${node.id}]`
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
widget.value = v
|
||||
widget.callback?.(
|
||||
widget.value,
|
||||
app.canvas,
|
||||
node,
|
||||
app.canvas.graph_mouse,
|
||||
{} as CanvasMouseEvent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,31 +342,6 @@ export class PrimitiveNode extends LGraphNode {
|
||||
}
|
||||
}
|
||||
|
||||
isValidWidgetLink(
|
||||
originSlot: number,
|
||||
targetNode: LGraphNode,
|
||||
targetWidget: IWidget
|
||||
) {
|
||||
const config2 = getConfig.call(targetNode, targetWidget.name) ?? [
|
||||
targetWidget.type,
|
||||
targetWidget.options || {}
|
||||
]
|
||||
if (!isConvertibleWidget(targetWidget, config2)) return false
|
||||
|
||||
const output = this.outputs[originSlot]
|
||||
if (
|
||||
!(
|
||||
output.widget?.[CONFIG] ??
|
||||
(output.widget?.[GET_CONFIG] as () => InputSpec)?.()
|
||||
)
|
||||
) {
|
||||
// No widget defined for this primitive yet so allow it
|
||||
return true
|
||||
}
|
||||
|
||||
return !!mergeIfValid.call(this, output, config2)
|
||||
}
|
||||
|
||||
#isValidConnection(input: INodeInputSlot, forceUpdate?: boolean) {
|
||||
// Only allow connections where the configs match
|
||||
const output = this.outputs?.[0]
|
||||
@@ -440,13 +410,6 @@ function getConfig(this: LGraphNode, widgetName: string) {
|
||||
)
|
||||
}
|
||||
|
||||
function isConvertibleWidget(widget: IWidget, config: InputSpec): boolean {
|
||||
return (
|
||||
// @ts-expect-error InputSpec is not typed correctly
|
||||
VALID_TYPES.includes(widget.type) || VALID_TYPES.includes(config[0])
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a widget to an input slot.
|
||||
* @deprecated Widget to socket conversion is no longer necessary, as they co-exist now.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"Comfy-Desktop_CheckForUpdates": {
|
||||
"label": "Check for Updates"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "Open Custom Nodes Folder"
|
||||
},
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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 / S’inscrire",
|
||||
"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",
|
||||
|
||||
@@ -108,6 +108,10 @@
|
||||
"Straight": "Droit"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "Couleur de fond initiale",
|
||||
"tooltip": "Contrôle la couleur de fond par défaut de la scène 3D. Ce paramètre détermine l'apparence du fond lors de la création d'un nouveau widget 3D, mais peut être ajusté individuellement pour chaque widget après la création."
|
||||
},
|
||||
"Comfy_Load3D_CameraType": {
|
||||
"name": "Type de Caméra",
|
||||
"options": {
|
||||
@@ -116,6 +120,22 @@
|
||||
},
|
||||
"tooltip": "Contrôle si la caméra est en perspective ou orthographique par défaut lorsqu'un nouveau widget 3D est créé. Ce défaut peut toujours être basculé individuellement pour chaque widget après sa création."
|
||||
},
|
||||
"Comfy_Load3D_LightAdjustmentIncrement": {
|
||||
"name": "Incrément d'ajustement de la lumière",
|
||||
"tooltip": "Contrôle la taille de l'incrément lors de l'ajustement de l'intensité lumineuse dans les scènes 3D. Une valeur de pas plus petite permet un contrôle plus précis des ajustements de lumière, tandis qu'une valeur plus grande entraîne des changements plus visibles à chaque ajustement."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensity": {
|
||||
"name": "Intensité lumineuse initiale",
|
||||
"tooltip": "Définit le niveau de luminosité par défaut de l’éclairage dans la scène 3D. Cette valeur détermine l’intensité avec laquelle les lumières illuminent les objets lors de la création d’un nouveau widget 3D, mais peut être ajustée individuellement pour chaque widget après la création."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMaximum": {
|
||||
"name": "Intensité lumineuse maximale",
|
||||
"tooltip": "Définit la valeur maximale autorisée pour l’intensité lumineuse dans les scènes 3D. Cela fixe la limite supérieure de luminosité pouvant être réglée lors de l’ajustement de l’éclairage dans tout widget 3D."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMinimum": {
|
||||
"name": "Intensité lumineuse minimale",
|
||||
"tooltip": "Définit la valeur minimale autorisée de l’intensité lumineuse pour les scènes 3D. Cela définit la limite inférieure de luminosité pouvant être réglée lors de l’ajustement de l’éclairage dans tout widget 3D."
|
||||
},
|
||||
"Comfy_Load3D_ShowGrid": {
|
||||
"name": "Afficher la Grille",
|
||||
"tooltip": "Basculer pour afficher la grille par défaut"
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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": "現在のワークフローを閉じる"
|
||||
},
|
||||
|
||||
@@ -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}文字以下でなければなりません",
|
||||
|
||||
@@ -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": "ツールチップ遅延"
|
||||
},
|
||||
|
||||
@@ -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": "현재 워크플로우 닫기"
|
||||
},
|
||||
|
||||