Compare commits
1 Commits
core/1.8
...
node-group
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1f30b96f9 |
43
.cursorrules
@@ -1,43 +0,0 @@
|
||||
// Vue 3 Composition API .cursorrules
|
||||
|
||||
// Vue 3 Composition API best practices
|
||||
const vue3CompositionApiBestPractices = [
|
||||
"Use setup() function for component logic",
|
||||
"Utilize ref and reactive for reactive state",
|
||||
"Implement computed properties with computed()",
|
||||
"Use watch and watchEffect for side effects",
|
||||
"Implement lifecycle hooks with onMounted, onUpdated, etc.",
|
||||
"Utilize provide/inject for dependency injection",
|
||||
]
|
||||
|
||||
// Folder structure
|
||||
const folderStructure = `
|
||||
src/
|
||||
components/
|
||||
constants/
|
||||
hooks/
|
||||
views/
|
||||
stores/
|
||||
services/
|
||||
App.vue
|
||||
main.ts
|
||||
`;
|
||||
|
||||
// Tailwind CSS best practices
|
||||
const tailwindCssBestPractices = [
|
||||
"Use Tailwind CSS for styling",
|
||||
"Implement responsive design with Tailwind CSS",
|
||||
]
|
||||
|
||||
// Additional instructions
|
||||
const additionalInstructions = `
|
||||
1. Leverage VueUse functions for performance-enhancing styles
|
||||
2. Use lodash for utility functions
|
||||
3. Use TypeScript for type safety
|
||||
4. Implement proper props and emits definitions
|
||||
5. Utilize Vue 3's Teleport component when needed
|
||||
6. Use Suspense for async components
|
||||
7. Implement proper error handling
|
||||
8. Follow Vue 3 style guide and naming conventions
|
||||
9. Use Vite for fast development and building
|
||||
`;
|
||||
5
.github/workflows/i18n-node-defs.yaml
vendored
@@ -42,7 +42,6 @@ jobs:
|
||||
Automated PR to update locales for node definitions
|
||||
|
||||
This PR was created automatically by the frontend update workflow.
|
||||
branch: update-locales-node-defs-${{ github.event.inputs.trigger_type }}-${{ github.run_id }}
|
||||
branch: update-locales-node-defs-{{ github.event.inputs.trigger_type }}-{{ github.run_id }}
|
||||
base: main
|
||||
labels: dependencies
|
||||
path: ComfyUI_frontend
|
||||
labels: dependencies
|
||||
2
.github/workflows/release.yaml
vendored
@@ -6,7 +6,6 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- core/*
|
||||
paths:
|
||||
- "package.json"
|
||||
|
||||
@@ -44,7 +43,6 @@ jobs:
|
||||
draft: true
|
||||
prerelease: false
|
||||
make_latest: "true"
|
||||
generate_release_notes: true
|
||||
publish_types:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
|
||||
3
.gitignore
vendored
@@ -16,9 +16,6 @@ dist-ssr
|
||||
.vscode/*
|
||||
*.code-workspace
|
||||
!.vscode/extensions.json
|
||||
!.vscode/tailwind.json
|
||||
!.vscode/settings.json.default
|
||||
!.vscode/launch.json.default
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
|
||||
16
.vscode/launch.json.default
vendored
@@ -1,16 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome on frontend dev",
|
||||
"url": "http://localhost:5173",
|
||||
"webRoot": "${workspaceFolder}/src",
|
||||
"sourceMaps": true,
|
||||
}
|
||||
]
|
||||
}
|
||||
5
.vscode/settings.json.default
vendored
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"css.customData": [
|
||||
".vscode/tailwind.json"
|
||||
]
|
||||
}
|
||||
55
.vscode/tailwind.json
vendored
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"version": 1.1,
|
||||
"atDirectives": [
|
||||
{
|
||||
"name": "@tailwind",
|
||||
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@apply",
|
||||
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#apply"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@responsive",
|
||||
"description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@screen",
|
||||
"description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#screen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@variants",
|
||||
"description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#variants"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"last_node_id": 1,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [20, 50],
|
||||
"size": [400, 200],
|
||||
"flags": { "collapsed": true },
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": null,
|
||||
"localized_name": "clip"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": null,
|
||||
"localized_name": "CONDITIONING"
|
||||
}
|
||||
],
|
||||
"properties": {},
|
||||
"widgets_values": ["Should not be displayed."]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"version": 0.4
|
||||
}
|
||||
|
Before Width: | Height: | Size: 488 B |
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,4 +1,4 @@
|
||||
import { Locator, expect } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { Keybinding } from '../src/types/keyBindingTypes'
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
@@ -66,71 +66,9 @@ test.describe('Missing models warning', () => {
|
||||
}, comfyPage.url)
|
||||
})
|
||||
|
||||
test('Should display a warning when missing models are found', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('missing_models')
|
||||
|
||||
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
|
||||
await expect(missingModelsWarning).toBeVisible()
|
||||
|
||||
const downloadButton = missingModelsWarning.getByLabel('Download')
|
||||
await expect(downloadButton).toBeVisible()
|
||||
})
|
||||
|
||||
test('Should not display a warning when no missing models are found', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const modelFoldersRes = {
|
||||
status: 200,
|
||||
body: JSON.stringify([
|
||||
{
|
||||
name: 'clip',
|
||||
folders: ['ComfyUI/models/clip']
|
||||
}
|
||||
])
|
||||
}
|
||||
comfyPage.page.route(
|
||||
'**/api/experiment/models',
|
||||
(route) => route.fulfill(modelFoldersRes),
|
||||
{ times: 1 }
|
||||
)
|
||||
|
||||
// Reload page to trigger indexing of model folders
|
||||
await comfyPage.setup()
|
||||
|
||||
const clipModelsRes = {
|
||||
status: 200,
|
||||
body: JSON.stringify([
|
||||
{
|
||||
name: 'fake_model.safetensors',
|
||||
pathIndex: 0
|
||||
}
|
||||
])
|
||||
}
|
||||
comfyPage.page.route(
|
||||
'**/api/experiment/models/clip',
|
||||
(route) => route.fulfill(clipModelsRes),
|
||||
{ times: 1 }
|
||||
)
|
||||
|
||||
await comfyPage.loadWorkflow('missing_models')
|
||||
|
||||
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
|
||||
await expect(missingModelsWarning).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('should show on tutorial workflow', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.TutorialCompleted', false)
|
||||
await comfyPage.setup({ clearStorage: true })
|
||||
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
|
||||
await expect(missingModelsWarning).toBeVisible()
|
||||
expect(await comfyPage.getSetting('Comfy.TutorialCompleted')).toBe(true)
|
||||
})
|
||||
|
||||
// Flaky test after parallelization
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/pull/1400
|
||||
test.skip('Should download missing model when clicking download button', async ({
|
||||
test.skip('Should display a warning when missing models are found', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// The fake_model.safetensors is served by
|
||||
@@ -148,49 +86,6 @@ test.describe('Missing models warning', () => {
|
||||
const download = await downloadPromise
|
||||
expect(download.suggestedFilename()).toBe('fake_model.safetensors')
|
||||
})
|
||||
|
||||
test.describe('Do not show again checkbox', () => {
|
||||
let checkbox: Locator
|
||||
let closeButton: Locator
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting(
|
||||
'Comfy.Workflow.ShowMissingModelsWarning',
|
||||
true
|
||||
)
|
||||
await comfyPage.loadWorkflow('missing_models')
|
||||
|
||||
checkbox = comfyPage.page.getByLabel("Don't show this again")
|
||||
closeButton = comfyPage.page.getByLabel('Close')
|
||||
})
|
||||
|
||||
test('Should disable warning dialog when checkbox is checked', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await checkbox.click()
|
||||
const changeSettingPromise = comfyPage.page.waitForRequest(
|
||||
'**/api/settings/Comfy.Workflow.ShowMissingModelsWarning'
|
||||
)
|
||||
await closeButton.click()
|
||||
await changeSettingPromise
|
||||
|
||||
const settingValue = await comfyPage.getSetting(
|
||||
'Comfy.Workflow.ShowMissingModelsWarning'
|
||||
)
|
||||
expect(settingValue).toBe(false)
|
||||
})
|
||||
|
||||
test('Should keep warning dialog enabled when checkbox is unchecked', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await closeButton.click()
|
||||
|
||||
const settingValue = await comfyPage.getSetting(
|
||||
'Comfy.Workflow.ShowMissingModelsWarning'
|
||||
)
|
||||
expect(settingValue).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Settings', () => {
|
||||
@@ -270,37 +165,3 @@ test.describe('Settings', () => {
|
||||
expect(request.postData()).toContain(JSON.stringify(expectedSetting))
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Feedback dialog', () => {
|
||||
test('Should open from topmenu help command', async ({ comfyPage }) => {
|
||||
// Open feedback dialog from top menu
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])
|
||||
|
||||
// Verify feedback dialog content is visible
|
||||
const feedbackHeader = comfyPage.page.getByRole('heading', {
|
||||
name: 'Feedback'
|
||||
})
|
||||
await expect(feedbackHeader).toBeVisible()
|
||||
})
|
||||
|
||||
test('Should close when close button clicked', async ({ comfyPage }) => {
|
||||
// Open feedback dialog
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])
|
||||
|
||||
const feedbackHeader = comfyPage.page.getByRole('heading', {
|
||||
name: 'Feedback'
|
||||
})
|
||||
|
||||
// Close feedback dialog
|
||||
await comfyPage.page
|
||||
.getByLabel('', { exact: true })
|
||||
.getByLabel('Close')
|
||||
.click()
|
||||
await feedbackHeader.waitFor({ state: 'hidden' })
|
||||
|
||||
// Verify dialog is closed
|
||||
await expect(feedbackHeader).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
test.describe('DOM Widget', () => {
|
||||
test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('collapsed_multiline')
|
||||
|
||||
expect(comfyPage.page.locator('.comfy-multiline-input')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Multiline textarea correctly collapses', async ({ comfyPage }) => {
|
||||
const multilineTextAreas = comfyPage.page.locator('.comfy-multiline-input')
|
||||
const firstMultiline = multilineTextAreas.first()
|
||||
const lastMultiline = multilineTextAreas.last()
|
||||
|
||||
await expect(firstMultiline).toBeVisible()
|
||||
await expect(lastMultiline).toBeVisible()
|
||||
|
||||
const nodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
|
||||
for (const node of nodes) {
|
||||
await node.click('collapse')
|
||||
}
|
||||
await expect(firstMultiline).not.toBeVisible()
|
||||
await expect(lastMultiline).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -904,9 +904,7 @@ export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
|
||||
'Comfy.NodeBadge.NodeSourceBadgeMode': NodeBadgeMode.None,
|
||||
// Disable tooltips by default to avoid flakiness.
|
||||
'Comfy.EnableTooltips': false,
|
||||
'Comfy.userId': userId,
|
||||
// Set tutorial completed to true to avoid loading the tutorial workflow.
|
||||
'Comfy.TutorialCompleted': true
|
||||
'Comfy.userId': userId
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
@@ -194,10 +194,6 @@ export class QueueSidebarTab extends SidebarTab {
|
||||
return this.root.locator('.no-results-placeholder')
|
||||
}
|
||||
|
||||
get galleryImage() {
|
||||
return this.page.locator('.galleria-image')
|
||||
}
|
||||
|
||||
private getToggleExpandButton(isExpanded: boolean) {
|
||||
const iconSelector = isExpanded ? '.pi-image' : '.pi-images'
|
||||
return this.root.locator(`.toggle-expanded-button ${iconSelector}`)
|
||||
@@ -260,24 +256,14 @@ export class QueueSidebarTab extends SidebarTab {
|
||||
|
||||
async openTaskPreview(taskIndex: number) {
|
||||
const previewButton = this.getTaskPreviewButton(taskIndex)
|
||||
await previewButton.hover()
|
||||
await previewButton.click()
|
||||
return this.galleryImage.waitFor({ state: 'visible' })
|
||||
return this.getGalleryImage(taskIndex).waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
getGalleryImage(imageFilename: string) {
|
||||
return this.galleryImage.and(this.page.getByAltText(imageFilename))
|
||||
}
|
||||
|
||||
getTaskImage(imageFilename: string) {
|
||||
return this.tasks.getByAltText(imageFilename)
|
||||
}
|
||||
|
||||
/** Trigger the queue store and tasks to update */
|
||||
async triggerTasksUpdate() {
|
||||
await this.page.evaluate(() => {
|
||||
window['app']['api'].dispatchCustomEvent('status', {
|
||||
exec_info: { queue_remaining: 0 }
|
||||
})
|
||||
})
|
||||
getGalleryImage(galleryItemIndex: number) {
|
||||
// Aria labels of Galleria items are 1-based indices
|
||||
const galleryLabel = `${galleryItemIndex + 1}`
|
||||
return this.page.getByLabel(galleryLabel).locator('.galleria-image')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,12 +132,11 @@ export default class TaskHistory {
|
||||
private addTask(task: HistoryTaskItem) {
|
||||
setPromptId(task)
|
||||
setQueueIndex(task)
|
||||
this.tasks.unshift(task) // Tasks are added to the front of the queue
|
||||
this.tasks.push(task)
|
||||
}
|
||||
|
||||
clearTasks(): this {
|
||||
clearTasks() {
|
||||
this.tasks = []
|
||||
return this
|
||||
}
|
||||
|
||||
withTask(
|
||||
@@ -156,7 +155,7 @@ export default class TaskHistory {
|
||||
/** Repeats the last task in the task history a specified number of times. */
|
||||
repeat(n: number): this {
|
||||
for (let i = 0; i < n; i++)
|
||||
this.addTask(structuredClone(this.tasks.at(0)) as HistoryTaskItem)
|
||||
this.addTask(structuredClone(this.tasks.at(-1)) as HistoryTaskItem)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
import { comfyPageFixture } from './fixtures/ComfyPage'
|
||||
import { webSocketFixture } from './fixtures/ws'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, webSocketFixture)
|
||||
|
||||
test.describe('Menu', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
@@ -945,61 +948,66 @@ test.describe.skip('Queue sidebar', () => {
|
||||
})
|
||||
|
||||
test.describe('Gallery', () => {
|
||||
const firstImage = 'example.webp'
|
||||
const secondImage = 'image32x32.webp'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage
|
||||
.setupHistory()
|
||||
.withTask([secondImage])
|
||||
.withTask([firstImage])
|
||||
.withTask(['example.webp'])
|
||||
.repeat(1)
|
||||
.setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
await comfyPage.menu.queueTab.openTaskPreview(0)
|
||||
})
|
||||
|
||||
test('displays gallery image after opening task preview', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.nextFrame()
|
||||
expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible()
|
||||
await comfyPage.menu.queueTab.openTaskPreview(0)
|
||||
expect(comfyPage.menu.queueTab.getGalleryImage(0)).toBeVisible()
|
||||
})
|
||||
|
||||
test('maintains active gallery item when new tasks are added', async ({
|
||||
comfyPage
|
||||
test('should maintain active gallery item when new tasks are added', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
const initialIndex = 0
|
||||
await comfyPage.menu.queueTab.openTaskPreview(initialIndex)
|
||||
|
||||
// Add a new task while the gallery is still open
|
||||
const newImage = 'image64x64.webp'
|
||||
comfyPage.setupHistory().withTask([newImage])
|
||||
await comfyPage.menu.queueTab.triggerTasksUpdate()
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
const newTask = comfyPage.menu.queueTab.tasks.getByAltText(newImage)
|
||||
await newTask.waitFor({ state: 'visible' })
|
||||
// The active gallery item should still be the initial image
|
||||
expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible()
|
||||
comfyPage.setupHistory().withTask(['example.webp'])
|
||||
await ws.trigger({
|
||||
type: 'status',
|
||||
data: {
|
||||
status: { exec_info: { queue_remaining: 0 } }
|
||||
}
|
||||
})
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
|
||||
// The index of all tasks increments when a new task is prepended
|
||||
const expectIndex = initialIndex + 1
|
||||
expect(comfyPage.menu.queueTab.getGalleryImage(expectIndex)).toBeVisible()
|
||||
})
|
||||
|
||||
test.describe('Gallery navigation', () => {
|
||||
const paths: {
|
||||
description: string
|
||||
path: ('Right' | 'Left')[]
|
||||
end: string
|
||||
expectIndex: number
|
||||
}[] = [
|
||||
{ description: 'Right', path: ['Right'], end: secondImage },
|
||||
{ description: 'Left', path: ['Right', 'Left'], end: firstImage },
|
||||
{ description: 'Left wrap', path: ['Left'], end: secondImage },
|
||||
{ description: 'Right wrap', path: ['Right', 'Right'], end: firstImage }
|
||||
{ description: 'Right', path: ['Right'], expectIndex: 1 },
|
||||
{ description: 'Left', path: ['Right', 'Left'], expectIndex: 0 },
|
||||
{ description: 'Right wrap', path: ['Right', 'Right'], expectIndex: 0 },
|
||||
{ description: 'Left wrap', path: ['Left'], expectIndex: 1 }
|
||||
]
|
||||
|
||||
paths.forEach(({ description, path, end }) => {
|
||||
paths.forEach(({ description, path, expectIndex }) => {
|
||||
test(`can navigate gallery ${description}`, async ({ comfyPage }) => {
|
||||
await comfyPage.menu.queueTab.openTaskPreview(0)
|
||||
for (const direction of path)
|
||||
await comfyPage.page.keyboard.press(`Arrow${direction}`, {
|
||||
delay: 256
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
expect(comfyPage.menu.queueTab.getGalleryImage(end)).toBeVisible()
|
||||
await comfyPage.page.keyboard.press(`Arrow${direction}`)
|
||||
|
||||
expect(
|
||||
comfyPage.menu.queueTab.getGalleryImage(expectIndex)
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -82,14 +82,10 @@ test.describe('Node search box', () => {
|
||||
test('Has correct aria-labels on search results', async ({ comfyPage }) => {
|
||||
const node = 'Load Checkpoint'
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await comfyPage.searchBox.input.waitFor({ state: 'visible' })
|
||||
await comfyPage.searchBox.input.fill(node)
|
||||
await comfyPage.searchBox.dropdown.waitFor({ state: 'visible' })
|
||||
// Wait for some time for the auto complete list to update.
|
||||
// The auto complete list is debounced and may take some time to update.
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
|
||||
const firstResult = comfyPage.searchBox.dropdown.locator('li').first()
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode(node)
|
||||
const firstResult = comfyPage.page
|
||||
.locator('li.p-autocomplete-option')
|
||||
.first()
|
||||
await expect(firstResult).toHaveAttribute('aria-label', node)
|
||||
})
|
||||
|
||||
|
||||
@@ -102,18 +102,6 @@ test.describe('Node Right Click Menu', () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
|
||||
await comfyPage.page.getByText('Convert Widget to Input').click()
|
||||
await comfyPage.nextFrame()
|
||||
// The submenu has an identical entry as the base menu - use last
|
||||
await comfyPage.page.getByText('Convert width to input').last().click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'right-click-node-widget-converted.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can convert widget without submenu', async ({ comfyPage }) => {
|
||||
// Right-click the width widget
|
||||
await comfyPage.rightClickEmptyLatentNode()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
|
||||
await comfyPage.page.getByText('Convert width to input').click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
|
||||
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 106 KiB |
12
global.d.ts
vendored
@@ -1,15 +1,3 @@
|
||||
declare const __COMFYUI_FRONTEND_VERSION__: string
|
||||
declare const __SENTRY_ENABLED__: boolean
|
||||
declare const __SENTRY_DSN__: string
|
||||
|
||||
interface Navigator {
|
||||
/**
|
||||
* Used by the electron API. This is a WICG non-standard API, but is guaranteed to exist in Electron.
|
||||
* It is `undefined` in Firefox and older browsers.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/windowControlsOverlay
|
||||
*/
|
||||
windowControlsOverlay?: {
|
||||
/** When `true`, the window is using custom window style. */
|
||||
visible: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
|
||||
function formatAndEslint(fileNames) {
|
||||
return [
|
||||
`prettier --write ${fileNames.join(' ')}`,
|
||||
`prettier --write ${fileNames.join(' ')} --plugin @trivago/prettier-plugin-sort-imports`,
|
||||
`eslint --fix ${fileNames.join(' ')}`
|
||||
]
|
||||
}
|
||||
|
||||
335
package-lock.json
generated
@@ -1,19 +1,18 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.8.14",
|
||||
"version": "1.7.10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.8.14",
|
||||
"version": "1.7.10",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.16",
|
||||
"@comfyorg/litegraph": "^0.8.62",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.7",
|
||||
"@comfyorg/litegraph": "^0.8.60",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
"@tiptap/core": "^2.10.4",
|
||||
"@tiptap/extension-link": "^2.10.4",
|
||||
@@ -33,10 +32,10 @@
|
||||
"loglevel": "^1.9.2",
|
||||
"pinia": "^2.1.7",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.2.5",
|
||||
"primevue": "^4.0.5",
|
||||
"three": "^0.170.0",
|
||||
"tiptap-markdown": "^0.8.10",
|
||||
"vue": "^3.5.13",
|
||||
"vue": "^3.4.31",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.4.3",
|
||||
"zod": "^3.23.8",
|
||||
@@ -1937,15 +1936,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@comfyorg/comfyui-electron-types": {
|
||||
"version": "0.4.16",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.16.tgz",
|
||||
"integrity": "sha512-AKy4WLVAuDka/Xjv8zrKwfU/wfRSQpFVE5DgxoLfvroCI0sw+rV1JqdL6xFVrYIoeprzbfKhQiyqlAWU+QgHyg==",
|
||||
"version": "0.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.7.tgz",
|
||||
"integrity": "sha512-APC3C4VZOo9W6h0xiAGxnsU9iNp3T8rN9w/5KmOCI0GUoKtKg5U2OaicTmnMwcDSQe5Jxflmej53GyJ1nH9oRw==",
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.8.62",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.62.tgz",
|
||||
"integrity": "sha512-El2QO8m1ky0YB/tUTDA3sorhtzZgBItMZ+j80eO9fAQb/ptToTUId+DxWLAZyz0qhA7RXJFA27emX+sdtWYO9g==",
|
||||
"version": "0.8.60",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.60.tgz",
|
||||
"integrity": "sha512-LkZalBcka1xVxkL7JnkF/1EzyvspLyrSthzyN9ZumWJw7kAaZkO9omraXv2t/UiFsqwMr5M/AV5UY915Vq8cxQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
@@ -3962,129 +3961,63 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@primeuix/forms": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@primeuix/forms/-/forms-0.0.2.tgz",
|
||||
"integrity": "sha512-DpecPQd/Qf/kav4LKCaIeGuT3AkwhJzuHCkLANTVlN/zBvo8KIj3OZHsCkm0zlIMVVnaJdtx1ULNlRQdudef+A==",
|
||||
"dependencies": {
|
||||
"@primeuix/utils": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primeuix/forms/node_modules/@primeuix/utils": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.3.2.tgz",
|
||||
"integrity": "sha512-B+nphqTQeq+i6JuICLdVWnDMjONome2sNz0xI65qIOyeB4EF12CoKRiCsxuZ5uKAkHi/0d1LqlQ9mIWRSdkavw==",
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primeuix/styled": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.3.2.tgz",
|
||||
"integrity": "sha512-ColZes0+/WKqH4ob2x8DyNYf1NENpe5ZguOvx5yCLxaP8EIMVhLjWLO/3umJiDnQU4XXMLkn2mMHHw+fhTX/mw==",
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.0.5.tgz",
|
||||
"integrity": "sha512-pVoGn/uPkVm/DyF3TR3EmH/pL/dP4nR42FcYbVduFq9VfO3KVeOEqvcCULHXos66RZO9MCbCFUoLy6ctf9GUGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@primeuix/utils": "^0.3.2"
|
||||
"@primeuix/utils": "^0.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primeuix/utils": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.3.2.tgz",
|
||||
"integrity": "sha512-B+nphqTQeq+i6JuICLdVWnDMjONome2sNz0xI65qIOyeB4EF12CoKRiCsxuZ5uKAkHi/0d1LqlQ9mIWRSdkavw==",
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.0.5.tgz",
|
||||
"integrity": "sha512-ntUiUgtRtkF8KuaxHffzhYxQxoXk6LAPHm7CVlFjdqS8Rx8xRkLkZVyo84E+pO2hcNFkOGVP/GxHhQ2s94O8zA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primevue/core": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.2.5.tgz",
|
||||
"integrity": "sha512-+oWBIQs5dLd2Ini4KEVOlvPILk989EHAskiFS3R/dz3jeOllJDMZFcSp8V9ddV0R3yDaPdLVkfHm2Q5t42kU2Q==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.0.5.tgz",
|
||||
"integrity": "sha512-DUCslDA93eUOVW0A1I3yoZgRLI4zmI2++loZQXbUF5jaXCwKiAza14+iyUU+cWH27VSq+jQnCEP9QJtPZiJJ0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@primeuix/styled": "^0.3.2",
|
||||
"@primeuix/utils": "^0.3.2"
|
||||
"@primeuix/styled": "^0.0.5",
|
||||
"@primeuix/utils": "^0.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primevue/forms": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@primevue/forms/-/forms-4.2.5.tgz",
|
||||
"integrity": "sha512-5jarJQ9Qv32bOo/0tY5bqR3JZI6+YmmoUQ2mjhVSbVElQsE4FNfhT7a7JwF+xgBPMPc8KWGNA1QB248HhPNVAg==",
|
||||
"dependencies": {
|
||||
"@primeuix/forms": "^0.0.2",
|
||||
"@primeuix/utils": "^0.3.2",
|
||||
"@primevue/core": "4.2.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primevue/forms/node_modules/@primeuix/styled": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.3.2.tgz",
|
||||
"integrity": "sha512-ColZes0+/WKqH4ob2x8DyNYf1NENpe5ZguOvx5yCLxaP8EIMVhLjWLO/3umJiDnQU4XXMLkn2mMHHw+fhTX/mw==",
|
||||
"dependencies": {
|
||||
"@primeuix/utils": "^0.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primevue/forms/node_modules/@primeuix/utils": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.3.2.tgz",
|
||||
"integrity": "sha512-B+nphqTQeq+i6JuICLdVWnDMjONome2sNz0xI65qIOyeB4EF12CoKRiCsxuZ5uKAkHi/0d1LqlQ9mIWRSdkavw==",
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primevue/forms/node_modules/@primevue/core": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.2.5.tgz",
|
||||
"integrity": "sha512-+oWBIQs5dLd2Ini4KEVOlvPILk989EHAskiFS3R/dz3jeOllJDMZFcSp8V9ddV0R3yDaPdLVkfHm2Q5t42kU2Q==",
|
||||
"dependencies": {
|
||||
"@primeuix/styled": "^0.3.2",
|
||||
"@primeuix/utils": "^0.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.3.0"
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primevue/icons": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.2.5.tgz",
|
||||
"integrity": "sha512-WFbUMZhQkXf/KmwcytkjGVeJ9aGEDXjP3uweOjQZMmRdEIxFnqYYpd90wE90JE1teZn3+TVnT4ZT7ejGyEXnFQ==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.0.5.tgz",
|
||||
"integrity": "sha512-ZxR9W1wlAE2fTtUhrHyeMx5t0jNyAgxDcHPm0cNXpX8q1XF95rSM/qb48QKXIBDBrJ/xs57BcyCNADP/VDPY4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@primeuix/utils": "^0.3.2",
|
||||
"@primevue/core": "4.2.5"
|
||||
"@primeuix/utils": "^0.0.5",
|
||||
"@primevue/core": "4.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primevue/themes": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@primevue/themes/-/themes-4.2.5.tgz",
|
||||
"integrity": "sha512-8F7yA36xYIKtNuAuyBdZZEks/bKDwlhH5WjpqGGB0FdwfAEoBYsynQ5sdqcT2Lb/NsajHmS5lc++Ttlvr1g1Lw==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@primevue/themes/-/themes-4.0.5.tgz",
|
||||
"integrity": "sha512-cRrAhOapOT8eFCTDwNdB/acg2ZEEkn7y6h6p188PYSjJsWnYK+D8eI1Js1ZB5HwWo4sWs3oR3Sy8bPwejnGbAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@primeuix/styled": "^0.3.2"
|
||||
"@primeuix/styled": "^0.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
@@ -5793,53 +5726,49 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
|
||||
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
|
||||
"license": "MIT",
|
||||
"version": "3.4.31",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.31.tgz",
|
||||
"integrity": "sha512-skOiodXWTV3DxfDhB4rOf3OGalpITLlgCeOwb+Y9GJpfQ8ErigdBUHomBzvG78JoVE8MJoQsb+qhZiHfKeNeEg==",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.25.3",
|
||||
"@vue/shared": "3.5.13",
|
||||
"@babel/parser": "^7.24.7",
|
||||
"@vue/shared": "3.4.31",
|
||||
"entities": "^4.5.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
|
||||
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
|
||||
"license": "MIT",
|
||||
"version": "3.4.31",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.31.tgz",
|
||||
"integrity": "sha512-wK424WMXsG1IGMyDGyLqB+TbmEBFM78hIsOJ9QwUVLGrcSk0ak6zYty7Pj8ftm7nEtdU/DGQxAXp0/lM/2cEpQ==",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
"@vue/compiler-core": "3.4.31",
|
||||
"@vue/shared": "3.4.31"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
|
||||
"integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
|
||||
"license": "MIT",
|
||||
"version": "3.4.31",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.31.tgz",
|
||||
"integrity": "sha512-einJxqEw8IIJxzmnxmJBuK2usI+lJonl53foq+9etB2HAzlPjAS/wa7r0uUpXw5ByX3/0uswVSrjNb17vJm1kQ==",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.25.3",
|
||||
"@vue/compiler-core": "3.5.13",
|
||||
"@vue/compiler-dom": "3.5.13",
|
||||
"@vue/compiler-ssr": "3.5.13",
|
||||
"@vue/shared": "3.5.13",
|
||||
"@babel/parser": "^7.24.7",
|
||||
"@vue/compiler-core": "3.4.31",
|
||||
"@vue/compiler-dom": "3.4.31",
|
||||
"@vue/compiler-ssr": "3.4.31",
|
||||
"@vue/shared": "3.4.31",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.11",
|
||||
"postcss": "^8.4.48",
|
||||
"magic-string": "^0.30.10",
|
||||
"postcss": "^8.4.38",
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
|
||||
"integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
|
||||
"license": "MIT",
|
||||
"version": "3.4.31",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.31.tgz",
|
||||
"integrity": "sha512-RtefmITAje3fJ8FSg1gwgDhdKhZVntIVbwupdyZDSifZTRMiWxWehAOTCc8/KZDnBOcYQ4/9VWxsTbd3wT0hAA==",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
"@vue/compiler-dom": "3.4.31",
|
||||
"@vue/shared": "3.4.31"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-vue2": {
|
||||
@@ -5883,6 +5812,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/language-core/node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
|
||||
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.25.3",
|
||||
"@vue/shared": "3.5.13",
|
||||
"entities": "^4.5.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/language-core/node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
|
||||
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/language-core/node_modules/@vue/shared": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
|
||||
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/language-core/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
@@ -5910,54 +5871,49 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
|
||||
"integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
|
||||
"license": "MIT",
|
||||
"version": "3.4.31",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.31.tgz",
|
||||
"integrity": "sha512-VGkTani8SOoVkZNds1PfJ/T1SlAIOf8E58PGAhIOUDYPC4GAmFA2u/E14TDAFcf3vVDKunc4QqCe/SHr8xC65Q==",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.13"
|
||||
"@vue/shared": "3.4.31"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
|
||||
"integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
|
||||
"license": "MIT",
|
||||
"version": "3.4.31",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.31.tgz",
|
||||
"integrity": "sha512-LDkztxeUPazxG/p8c5JDDKPfkCDBkkiNLVNf7XZIUnJ+66GVGkP+TIh34+8LtPisZ+HMWl2zqhIw0xN5MwU1cw==",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
"@vue/reactivity": "3.4.31",
|
||||
"@vue/shared": "3.4.31"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
|
||||
"integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
|
||||
"license": "MIT",
|
||||
"version": "3.4.31",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.31.tgz",
|
||||
"integrity": "sha512-2Auws3mB7+lHhTFCg8E9ZWopA6Q6L455EcU7bzcQ4x6Dn4cCPuqj6S2oBZgN2a8vJRS/LSYYxwFFq2Hlx3Fsaw==",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.13",
|
||||
"@vue/runtime-core": "3.5.13",
|
||||
"@vue/shared": "3.5.13",
|
||||
"@vue/reactivity": "3.4.31",
|
||||
"@vue/runtime-core": "3.4.31",
|
||||
"@vue/shared": "3.4.31",
|
||||
"csstype": "^3.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
|
||||
"integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
|
||||
"license": "MIT",
|
||||
"version": "3.4.31",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.31.tgz",
|
||||
"integrity": "sha512-D5BLbdvrlR9PE3by9GaUp1gQXlCNadIZytMIb8H2h3FMWJd4oUfkUTEH2wAr3qxoRz25uxbTcbqd3WKlm9EHQA==",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
"@vue/compiler-ssr": "3.4.31",
|
||||
"@vue/shared": "3.4.31"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.5.13"
|
||||
"vue": "3.4.31"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
|
||||
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
|
||||
"license": "MIT"
|
||||
"version": "3.4.31",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.31.tgz",
|
||||
"integrity": "sha512-Yp3wtJk//8cO4NItOPpi3QkLExAr/aLBGZMmTtW9WpdwBCJpRM6zj9WgWktXAl8IDIozwNMByT45JP3tO3ACWA=="
|
||||
},
|
||||
"node_modules/@vue/test-utils": {
|
||||
"version": "2.4.6",
|
||||
@@ -7891,8 +7847,7 @@
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "3.0.2",
|
||||
@@ -14472,16 +14427,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
@@ -15216,10 +15170,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
|
||||
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
@@ -15383,9 +15336,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
|
||||
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
|
||||
"version": "8.4.47",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
||||
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -15400,10 +15353,9 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.8",
|
||||
"picocolors": "^1.1.1",
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.1.0",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -15576,15 +15528,15 @@
|
||||
"integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw=="
|
||||
},
|
||||
"node_modules/primevue": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.2.5.tgz",
|
||||
"integrity": "sha512-7UMOIJvdFz4jQyhC76yhNdSlHtXvVpmE2JSo2ndUTBWjWJOkYyT562rQ4ayO+bMdJLtzBGqgY64I9ZfEvNd7vQ==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.0.5.tgz",
|
||||
"integrity": "sha512-MALszGIZ5SnEQy1XeZLBFhpMXQ1OS7D1U7H+l/JAX5U46RQ1vufo7NAiWbbV5/ADjPGw4uLplqMQxujkksNY2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@primeuix/styled": "^0.3.2",
|
||||
"@primeuix/utils": "^0.3.2",
|
||||
"@primevue/core": "4.2.5",
|
||||
"@primevue/icons": "4.2.5"
|
||||
"@primeuix/styled": "^0.0.5",
|
||||
"@primeuix/utils": "^0.0.5",
|
||||
"@primevue/core": "4.0.5",
|
||||
"@primevue/icons": "4.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
@@ -18864,16 +18816,15 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
|
||||
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
|
||||
"license": "MIT",
|
||||
"version": "3.4.31",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.31.tgz",
|
||||
"integrity": "sha512-njqRrOy7W3YLAlVqSKpBebtZpDVg21FPoaq1I7f/+qqBThK9ChAIjkRWgeP6Eat+8C+iia4P3OYqpATP21BCoQ==",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.13",
|
||||
"@vue/compiler-sfc": "3.5.13",
|
||||
"@vue/runtime-dom": "3.5.13",
|
||||
"@vue/server-renderer": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
"@vue/compiler-dom": "3.4.31",
|
||||
"@vue/compiler-sfc": "3.4.31",
|
||||
"@vue/runtime-dom": "3.4.31",
|
||||
"@vue/server-renderer": "3.4.31",
|
||||
"@vue/shared": "3.4.31"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
|
||||
13
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.8.14",
|
||||
"version": "1.7.10",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -83,10 +83,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.16",
|
||||
"@comfyorg/litegraph": "^0.8.62",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.7",
|
||||
"@comfyorg/litegraph": "^0.8.60",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
"@tiptap/core": "^2.10.4",
|
||||
"@tiptap/extension-link": "^2.10.4",
|
||||
@@ -106,10 +105,10 @@
|
||||
"loglevel": "^1.9.2",
|
||||
"pinia": "^2.1.7",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.2.5",
|
||||
"primevue": "^4.0.5",
|
||||
"three": "^0.170.0",
|
||||
"tiptap-markdown": "^0.8.10",
|
||||
"vue": "^3.5.13",
|
||||
"vue": "^3.4.31",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.4.3",
|
||||
"zod": "^3.23.8",
|
||||
|
||||
@@ -266,7 +266,7 @@
|
||||
],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"v1-5-pruned-emaonly-fp16.safetensors"
|
||||
"v1-5-pruned-emaonly.safetensors"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -349,8 +349,8 @@
|
||||
"extra": {},
|
||||
"version": 0.4,
|
||||
"models": [{
|
||||
"name": "v1-5-pruned-emaonly-fp16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/stable-diffusion-v1-5-archive/resolve/main/v1-5-pruned-emaonly-fp16.safetensors?download=true",
|
||||
"name": "v1-5-pruned-emaonly.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/stable-diffusion-v1-5-archive/resolve/main/v1-5-pruned-emaonly.safetensors?download=true",
|
||||
"directory": "checkpoints"
|
||||
}]
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { execSync } from 'child_process'
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
const packageName = '@comfyorg/comfyui-electron-types'
|
||||
const description = 'desktop API types'
|
||||
|
||||
try {
|
||||
// Create a new branch
|
||||
console.log('Creating new branch...')
|
||||
const date = new Date()
|
||||
const isoDate = date.toISOString().split('T')[0]
|
||||
const timestamp = date.getTime()
|
||||
const branchName = `update-electron-types-${isoDate}-${timestamp}`
|
||||
execSync(`git checkout -b ${branchName} -t origin/main`, { stdio: 'inherit' })
|
||||
|
||||
// Update npm package to latest version
|
||||
console.log(`Updating ${description}...`)
|
||||
execSync(`npm install ${packageName}@latest`, {
|
||||
stdio: 'inherit'
|
||||
})
|
||||
|
||||
// Get the new version from package.json
|
||||
const packageLock = JSON.parse(readFileSync('./package-lock.json', 'utf8'))
|
||||
const newVersion = packageLock.packages[`node_modules/${packageName}`].version
|
||||
|
||||
// Stage changes
|
||||
const message = `[chore] Update electron-types to ${newVersion}`
|
||||
execSync('git add package.json package-lock.json', { stdio: 'inherit' })
|
||||
execSync(`git commit -m "${message}"`, { stdio: 'inherit' })
|
||||
|
||||
// Create the PR
|
||||
console.log('Creating PR...')
|
||||
execSync(
|
||||
`gh pr create --title "${message}" --label "dependencies" --body "Automated update of ${description} to version ${newVersion}."`,
|
||||
{ stdio: 'inherit' }
|
||||
)
|
||||
|
||||
console.log(
|
||||
`✅ Successfully created PR for ${description} update to ${newVersion}`
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('❌ Error during update process:', error.message)
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
import config from '@/config'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
import { electronAPI, isElectron } from './utils/envUtil'
|
||||
import { isElectron, showNativeMenu } from './utils/envUtil'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const isLoading = computed<boolean>(() => workspaceStore.spinner)
|
||||
@@ -34,7 +34,7 @@ const showContextMenu = (event: PointerEvent) => {
|
||||
case target instanceof HTMLTextAreaElement:
|
||||
case target instanceof HTMLInputElement && target.type === 'text':
|
||||
// TODO: Context input menu explicitly for text input
|
||||
electronAPI()?.showContextMenu({ type: 'text' })
|
||||
showNativeMenu({ type: 'text' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,30 +765,6 @@ audio.comfy-audio.empty-audio-widget {
|
||||
padding: var(--comfy-tree-explorer-item-padding) !important;
|
||||
}
|
||||
|
||||
/* Load3d styles */
|
||||
.comfy-load-3d,
|
||||
.comfy-load-3d-animation,
|
||||
.comfy-preview-3d,
|
||||
.comfy-preview-3d-animation{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: transparent;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.comfy-load-3d canvas,
|
||||
.comfy-load-3d-animation canvas,
|
||||
.comfy-preview-3d canvas,
|
||||
.comfy-preview-3d-animation canvas{
|
||||
display: flex;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* End of Load3d styles */
|
||||
|
||||
/* [Desktop] Electron window specific styles */
|
||||
.app-drag {
|
||||
app-region: drag;
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</template>
|
||||
<template #item="{ item }">
|
||||
<Button
|
||||
:label="String(item.label)"
|
||||
:label="item.label"
|
||||
:icon="item.icon"
|
||||
:severity="item.key === queueMode ? 'primary' : 'secondary'"
|
||||
size="small"
|
||||
|
||||
@@ -20,16 +20,11 @@ const terminalCreated = (
|
||||
let offData: IDisposable
|
||||
let offOutput: () => void
|
||||
|
||||
useAutoSize({
|
||||
root,
|
||||
autoRows: true,
|
||||
autoCols: true,
|
||||
onResize: () => {
|
||||
// If we aren't visible, don't resize
|
||||
if (!terminal.element?.offsetParent) return
|
||||
useAutoSize(root, true, true, () => {
|
||||
// If we aren't visible, don't resize
|
||||
if (!terminal.element?.offsetParent) return
|
||||
|
||||
terminalApi.resize(terminal.cols, terminal.rows)
|
||||
}
|
||||
terminalApi.resize(terminal.cols, terminal.rows)
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -29,12 +29,7 @@ const terminalCreated = (
|
||||
{ terminal, useAutoSize }: ReturnType<typeof useTerminal>,
|
||||
root: Ref<HTMLElement>
|
||||
) => {
|
||||
// `autoCols` is false because we don't want the progress bar in the terminal
|
||||
// to render incorrectly as the progress bar is rendered based on the
|
||||
// server's terminal size.
|
||||
// Apply a min cols of 80 for colab environments
|
||||
// See https://github.com/comfyanonymous/ComfyUI/issues/6396
|
||||
useAutoSize({ root, autoRows: true, autoCols: false, minCols: 80 })
|
||||
useAutoSize(root, true, false)
|
||||
|
||||
const update = (entries: Array<LogEntry>, size?: TerminalSize) => {
|
||||
if (size) {
|
||||
|
||||
40
src/components/common/CheckboxGroup.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div :class="['flex flex-wrap', $attrs.class]">
|
||||
<div
|
||||
v-for="checkbox in checkboxes"
|
||||
:key="checkbox.value"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<Checkbox
|
||||
v-model="internalSelection"
|
||||
:inputId="checkbox.value"
|
||||
:value="checkbox.value"
|
||||
/>
|
||||
<label :for="checkbox.value" class="ml-2">{{ checkbox.label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface CheckboxItem {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
checkboxes: CheckboxItem[]
|
||||
modelValue: string[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string[]): void
|
||||
}>()
|
||||
|
||||
const internalSelection = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: string[]) => emit('update:modelValue', value)
|
||||
})
|
||||
</script>
|
||||
@@ -42,7 +42,7 @@
|
||||
v-else
|
||||
class="pi pi-palette"
|
||||
:style="{ fontSize: '1.2rem' }"
|
||||
v-tooltip="$t('color.custom')"
|
||||
v-tooltip="$t('g.customColor')"
|
||||
></i>
|
||||
</template>
|
||||
</SelectButton>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
:label="$t('g.download') + ' (' + fileSize + ')'"
|
||||
size="small"
|
||||
outlined
|
||||
:disabled="!!props.error"
|
||||
:disabled="props.error"
|
||||
@click="triggerDownload"
|
||||
v-if="status === null || status === 'error'"
|
||||
icon="pi pi-download"
|
||||
@@ -30,7 +30,7 @@
|
||||
v-if="status === 'in_progress' || status === 'paused'"
|
||||
>
|
||||
<!-- Temporary fix for issue when % only comes into view only if the progress bar is large enough
|
||||
https://comfy-organization.slack.com/archives/C07H3GLKDPF/p1731551013385499
|
||||
https://comfy-organization.slack.com/archives/C07H3GLKDPF/p1731551013385499
|
||||
-->
|
||||
<ProgressBar
|
||||
class="flex-1"
|
||||
@@ -42,7 +42,7 @@
|
||||
class="file-action-button"
|
||||
size="small"
|
||||
outlined
|
||||
:disabled="!!props.error"
|
||||
:disabled="props.error"
|
||||
@click="triggerPauseDownload"
|
||||
v-if="status === 'in_progress'"
|
||||
icon="pi pi-pause-circle"
|
||||
@@ -53,7 +53,7 @@
|
||||
class="file-action-button"
|
||||
size="small"
|
||||
outlined
|
||||
:disabled="!!props.error"
|
||||
:disabled="props.error"
|
||||
@click="triggerResumeDownload"
|
||||
v-if="status === 'paused'"
|
||||
icon="pi pi-play-circle"
|
||||
@@ -64,7 +64,7 @@
|
||||
class="file-action-button"
|
||||
size="small"
|
||||
outlined
|
||||
:disabled="!!props.error"
|
||||
:disabled="props.error"
|
||||
@click="triggerCancelDownload"
|
||||
icon="pi pi-times-circle"
|
||||
severity="danger"
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
:label="$t('g.download') + ' (' + fileSize + ')'"
|
||||
size="small"
|
||||
outlined
|
||||
:disabled="!!props.error"
|
||||
:disabled="props.error"
|
||||
:title="props.url"
|
||||
@click="download.triggerBrowserDownload"
|
||||
/>
|
||||
|
||||
@@ -1,17 +1,35 @@
|
||||
<template>
|
||||
<div class="color-picker-wrapper flex items-center gap-2">
|
||||
<ColorPicker v-model="modelValue" />
|
||||
<ColorPicker v-model="modelValue">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between p-2">
|
||||
<span>{{ props.label }}</span>
|
||||
<Button
|
||||
v-if="props.defaultValue"
|
||||
icon="pi pi-refresh"
|
||||
text
|
||||
size="small"
|
||||
@click="resetColor"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ColorPicker>
|
||||
<InputText v-model="modelValue" class="w-28" :placeholder="label" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import ColorPicker from 'primevue/colorpicker'
|
||||
import InputText from 'primevue/inputtext'
|
||||
|
||||
const modelValue = defineModel<string>('modelValue')
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
defaultValue?: string
|
||||
label?: string
|
||||
}>()
|
||||
|
||||
const resetColor = () => {
|
||||
modelValue.value = props.defaultValue || '#000000'
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -35,7 +35,6 @@ import CustomFormValue from '@/components/common/CustomFormValue.vue'
|
||||
import FormColorPicker from '@/components/common/FormColorPicker.vue'
|
||||
import FormImageUpload from '@/components/common/FormImageUpload.vue'
|
||||
import InputSlider from '@/components/common/InputSlider.vue'
|
||||
import UrlInput from '@/components/common/UrlInput.vue'
|
||||
import { FormItem } from '@/types/settingTypes'
|
||||
|
||||
const formValue = defineModel<any>('formValue')
|
||||
@@ -92,8 +91,6 @@ function getFormComponent(item: FormItem): Component {
|
||||
return FormImageUpload
|
||||
case 'color':
|
||||
return FormColorPicker
|
||||
case 'url':
|
||||
return UrlInput
|
||||
default:
|
||||
return InputText
|
||||
}
|
||||
|
||||
66
src/components/common/LogTerminal.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<!-- A simple read-only terminal component that displays logs. -->
|
||||
<template>
|
||||
<div class="p-terminal rounded-none h-full w-full">
|
||||
<ScrollPanel class="h-full w-full" ref="scrollPanelRef">
|
||||
<pre class="px-4 whitespace-pre-wrap">{{ log }}</pre>
|
||||
</ScrollPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
fetchLogs: () => Promise<string>
|
||||
fetchInterval: number
|
||||
}>()
|
||||
|
||||
const log = ref<string>('')
|
||||
const scrollPanelRef = ref<InstanceType<typeof ScrollPanel> | null>(null)
|
||||
/**
|
||||
* Whether the user has scrolled to the bottom of the terminal.
|
||||
* This is used to prevent the terminal from scrolling to the bottom
|
||||
* when new logs are fetched.
|
||||
*/
|
||||
const scrolledToBottom = ref(false)
|
||||
|
||||
let intervalId: number = 0
|
||||
|
||||
onMounted(async () => {
|
||||
const element = scrollPanelRef.value?.$el
|
||||
const scrollContainer = element?.querySelector('.p-scrollpanel-content')
|
||||
|
||||
if (scrollContainer) {
|
||||
scrollContainer.addEventListener('scroll', () => {
|
||||
scrolledToBottom.value =
|
||||
scrollContainer.scrollTop + scrollContainer.clientHeight ===
|
||||
scrollContainer.scrollHeight
|
||||
})
|
||||
}
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (scrollContainer) {
|
||||
scrollContainer.scrollTop = scrollContainer.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
watch(log, () => {
|
||||
if (scrolledToBottom.value) {
|
||||
scrollToBottom()
|
||||
}
|
||||
})
|
||||
|
||||
const fetchLogs = async () => {
|
||||
log.value = await props.fetchLogs()
|
||||
}
|
||||
|
||||
await fetchLogs()
|
||||
scrollToBottom()
|
||||
intervalId = window.setInterval(fetchLogs, props.fetchInterval)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.clearInterval(intervalId)
|
||||
})
|
||||
</script>
|
||||
@@ -1,53 +0,0 @@
|
||||
<!--
|
||||
A refresh button that disables and shows a progress spinner whilst active.
|
||||
|
||||
Usage:
|
||||
```vue
|
||||
<RefreshButton
|
||||
v-model="isRefreshing"
|
||||
:outlined="false"
|
||||
@refresh="refresh"
|
||||
/>
|
||||
```
|
||||
-->
|
||||
<template>
|
||||
<Button
|
||||
class="relative p-button-icon-only"
|
||||
:outlined="props.outlined"
|
||||
:severity="props.severity"
|
||||
:disabled="active || props.disabled"
|
||||
@click="(event) => $emit('refresh', event)"
|
||||
>
|
||||
<span
|
||||
class="p-button-icon pi pi-refresh transition-all"
|
||||
:class="{ 'opacity-0': active }"
|
||||
data-pc-section="icon"
|
||||
></span>
|
||||
<span class="p-button-label" data-pc-section="label"> </span>
|
||||
<ProgressSpinner v-show="active" class="absolute w-1/2 h-1/2" />
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
|
||||
import { VueSeverity } from '@/types/primeVueTypes'
|
||||
|
||||
// Properties
|
||||
interface Props {
|
||||
outlined?: boolean
|
||||
disabled?: boolean
|
||||
severity?: VueSeverity
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
outlined: true,
|
||||
severity: 'secondary'
|
||||
})
|
||||
|
||||
// Model
|
||||
const active = defineModel<boolean>({ required: true })
|
||||
|
||||
// Emits
|
||||
defineEmits(['refresh'])
|
||||
</script>
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div>
|
||||
<div :class="props.class">
|
||||
<IconField>
|
||||
<Button
|
||||
v-if="filterIcon"
|
||||
v-if="props.filterIcon"
|
||||
class="p-inputicon filter-button"
|
||||
:icon="filterIcon"
|
||||
:icon="props.filterIcon"
|
||||
text
|
||||
severity="contrast"
|
||||
@click="$emit('showFilter', $event)"
|
||||
@@ -12,12 +12,12 @@
|
||||
<InputText
|
||||
class="search-box-input w-full"
|
||||
@input="handleInput"
|
||||
:modelValue="modelValue"
|
||||
:placeholder="placeholder"
|
||||
:modelValue="props.modelValue"
|
||||
:placeholder="props.placeholder"
|
||||
/>
|
||||
<InputIcon v-if="!modelValue" :class="icon" />
|
||||
<InputIcon v-if="!props.modelValue" :class="props.icon" />
|
||||
<Button
|
||||
v-if="modelValue"
|
||||
v-if="props.modelValue"
|
||||
class="p-inputicon clear-button"
|
||||
icon="pi pi-times"
|
||||
text
|
||||
@@ -47,36 +47,40 @@ import Button from 'primevue/button'
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { toRefs } from 'vue'
|
||||
|
||||
import type { SearchFilter } from './SearchFilterChip.vue'
|
||||
import SearchFilterChip from './SearchFilterChip.vue'
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
placeholder = 'Search...',
|
||||
icon = 'pi pi-search',
|
||||
debounceTime = 300,
|
||||
filterIcon,
|
||||
filters = []
|
||||
} = defineProps<{
|
||||
modelValue: string
|
||||
placeholder?: string
|
||||
icon?: string
|
||||
debounceTime?: number
|
||||
filterIcon?: string
|
||||
filters?: TFilter[]
|
||||
}>()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
class?: string
|
||||
modelValue: string
|
||||
placeholder?: string
|
||||
icon?: string
|
||||
debounceTime?: number
|
||||
filterIcon?: string
|
||||
filters?: TFilter[]
|
||||
}>(),
|
||||
{
|
||||
placeholder: 'Search...',
|
||||
icon: 'pi pi-search',
|
||||
debounceTime: 300
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
(e: 'search', value: string, filters: TFilter[]): void
|
||||
(e: 'showFilter', event: Event): void
|
||||
(e: 'removeFilter', filter: TFilter): void
|
||||
}>()
|
||||
const { filters } = toRefs(props)
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:modelValue',
|
||||
'search',
|
||||
'showFilter',
|
||||
'removeFilter'
|
||||
])
|
||||
|
||||
const emitSearch = debounce((value: string) => {
|
||||
emit('search', value, filters)
|
||||
}, debounceTime)
|
||||
emit('search', value, props.filters)
|
||||
}, props.debounceTime)
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
v-for="device in props.stats.devices"
|
||||
:key="device.index"
|
||||
:header="device.name"
|
||||
:value="device.index"
|
||||
>
|
||||
<DeviceInfo :device="device" />
|
||||
</TabPanel>
|
||||
|
||||
@@ -8,12 +8,9 @@
|
||||
selectionMode="single"
|
||||
:pt="{
|
||||
nodeLabel: 'tree-explorer-node-label',
|
||||
nodeContent: ({ context }) => ({
|
||||
class: 'group/tree-node',
|
||||
onClick: (e: MouseEvent) =>
|
||||
onNodeContentClick(e, context.node as RenderedTreeExplorerNode),
|
||||
onContextmenu: (e: MouseEvent) =>
|
||||
handleContextMenu(e, context.node as RenderedTreeExplorerNode)
|
||||
nodeContent: ({ props }) => ({
|
||||
onClick: (e: MouseEvent) => onNodeContentClick(e, props.node),
|
||||
onContextmenu: (e: MouseEvent) => handleContextMenu(props.node, e)
|
||||
}),
|
||||
nodeToggleButton: () => ({
|
||||
onClick: (e: MouseEvent) => {
|
||||
@@ -155,7 +152,7 @@ const menuItems = computed<MenuItem[]>(() =>
|
||||
}))
|
||||
)
|
||||
|
||||
const handleContextMenu = (e: MouseEvent, node: RenderedTreeExplorerNode) => {
|
||||
const handleContextMenu = (node: RenderedTreeExplorerNode, e: MouseEvent) => {
|
||||
menuTargetNode.value = node
|
||||
emit('contextMenu', node, e)
|
||||
if (menuItems.value.filter((item) => item.visible).length > 0) {
|
||||
|
||||
@@ -27,16 +27,13 @@
|
||||
class="leaf-count-badge"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="node-actions motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
|
||||
>
|
||||
<div class="node-actions">
|
||||
<slot name="actions" :node="props.node"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview'
|
||||
import Badge from 'primevue/badge'
|
||||
import { Ref, computed, inject, ref } from 'vue'
|
||||
|
||||
@@ -105,17 +102,7 @@ if (props.node.draggable) {
|
||||
}
|
||||
},
|
||||
onDragStart: () => emit('dragStart', props.node),
|
||||
onDrop: () => emit('dragEnd', props.node),
|
||||
onGenerateDragPreview: props.node.renderDragPreview
|
||||
? ({ nativeSetDragImage }) => {
|
||||
setCustomNativeDragPreview({
|
||||
render: ({ container }) => {
|
||||
return props.node.renderDragPreview(props.node, container)
|
||||
},
|
||||
nativeSetDragImage
|
||||
})
|
||||
}
|
||||
: undefined
|
||||
onDrop: () => emit('dragEnd', props.node)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
<template>
|
||||
<IconField class="w-full">
|
||||
<InputText
|
||||
v-bind="$attrs"
|
||||
:model-value="internalValue"
|
||||
class="w-full"
|
||||
:invalid="validationState === ValidationState.INVALID"
|
||||
@update:model-value="handleInput"
|
||||
@blur="handleBlur"
|
||||
/>
|
||||
<InputIcon
|
||||
:class="{
|
||||
'pi pi-spin pi-spinner text-neutral-400':
|
||||
validationState === ValidationState.LOADING,
|
||||
'pi pi-check text-green-500 cursor-pointer':
|
||||
validationState === ValidationState.VALID,
|
||||
'pi pi-times text-red-500 cursor-pointer':
|
||||
validationState === ValidationState.INVALID
|
||||
}"
|
||||
@click="validateUrl(props.modelValue)"
|
||||
/>
|
||||
</IconField>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { isValidUrl } from '@/utils/formatUtil'
|
||||
import { checkUrlReachable } from '@/utils/networkUtil'
|
||||
import { ValidationState } from '@/utils/validationUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
validateUrlFn?: (url: string) => Promise<boolean>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
'state-change': [state: ValidationState]
|
||||
}>()
|
||||
|
||||
const validationState = ref<ValidationState>(ValidationState.IDLE)
|
||||
|
||||
// Add internal value state
|
||||
const internalValue = ref(props.modelValue)
|
||||
|
||||
// Watch for external modelValue changes
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (newValue: string) => {
|
||||
internalValue.value = newValue
|
||||
await validateUrl(newValue)
|
||||
}
|
||||
)
|
||||
|
||||
watch(validationState, (newState) => {
|
||||
emit('state-change', newState)
|
||||
})
|
||||
|
||||
// Validate on mount
|
||||
onMounted(async () => {
|
||||
await validateUrl(props.modelValue)
|
||||
})
|
||||
|
||||
const handleInput = (value: string) => {
|
||||
// Update internal value without emitting
|
||||
internalValue.value = value
|
||||
// Reset validation state when user types
|
||||
validationState.value = ValidationState.IDLE
|
||||
}
|
||||
|
||||
const handleBlur = async () => {
|
||||
// Emit the update only on blur
|
||||
emit('update:modelValue', internalValue.value)
|
||||
}
|
||||
|
||||
// Default validation implementation
|
||||
const defaultValidateUrl = async (url: string): Promise<boolean> => {
|
||||
if (!isValidUrl(url)) return false
|
||||
try {
|
||||
return await checkUrlReachable(url)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const validateUrl = async (value: string) => {
|
||||
if (validationState.value === ValidationState.LOADING) return
|
||||
|
||||
const url = value.trim()
|
||||
|
||||
// Reset state
|
||||
validationState.value = ValidationState.IDLE
|
||||
|
||||
// Skip validation if empty
|
||||
if (!url) return
|
||||
|
||||
validationState.value = ValidationState.LOADING
|
||||
try {
|
||||
const isValid = await (props.validateUrlFn ?? defaultValidateUrl)(url)
|
||||
validationState.value = isValid
|
||||
? ValidationState.VALID
|
||||
: ValidationState.INVALID
|
||||
} catch {
|
||||
validationState.value = ValidationState.INVALID
|
||||
}
|
||||
}
|
||||
|
||||
// Add inheritAttrs option to prevent attrs from being applied to root element
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
</script>
|
||||
@@ -1,158 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { createApp, nextTick } from 'vue'
|
||||
|
||||
import UrlInput from '../UrlInput.vue'
|
||||
|
||||
describe('UrlInput', () => {
|
||||
beforeEach(() => {
|
||||
const app = createApp({})
|
||||
app.use(PrimeVue)
|
||||
})
|
||||
|
||||
const mountComponent = (props: any, options = {}) => {
|
||||
return mount(UrlInput, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: { IconField, InputIcon, InputText }
|
||||
},
|
||||
props,
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
it('passes through additional attributes to input element', () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: '',
|
||||
placeholder: 'Enter URL',
|
||||
disabled: true
|
||||
})
|
||||
|
||||
expect(wrapper.find('input').attributes('disabled')).toBe('')
|
||||
})
|
||||
|
||||
it('emits update:modelValue on blur', async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: '',
|
||||
placeholder: 'Enter URL'
|
||||
})
|
||||
|
||||
const input = wrapper.find('input')
|
||||
await input.setValue('https://test.com')
|
||||
await input.trigger('blur')
|
||||
|
||||
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([
|
||||
'https://test.com'
|
||||
])
|
||||
})
|
||||
|
||||
it('renders spinner when validation is loading', async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: '',
|
||||
placeholder: 'Enter URL',
|
||||
validateUrlFn: () =>
|
||||
new Promise(() => {
|
||||
// Never resolves, simulating perpetual loading state
|
||||
})
|
||||
})
|
||||
|
||||
wrapper.setProps({ modelValue: 'https://test.com' })
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.pi-spinner').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders check icon when validation is valid', async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: '',
|
||||
placeholder: 'Enter URL',
|
||||
validateUrlFn: () => Promise.resolve(true)
|
||||
})
|
||||
|
||||
wrapper.setProps({ modelValue: 'https://test.com' })
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.pi-check').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders cross icon when validation is invalid', async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: '',
|
||||
placeholder: 'Enter URL',
|
||||
validateUrlFn: () => Promise.resolve(false)
|
||||
})
|
||||
|
||||
wrapper.setProps({ modelValue: 'https://test.com' })
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.pi-times').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('validates on mount', async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'https://test.com',
|
||||
validateUrlFn: () => Promise.resolve(true)
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.pi-check').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('triggers validation when clicking the validation icon', async () => {
|
||||
let validationCount = 0
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'https://test.com',
|
||||
validateUrlFn: () => {
|
||||
validationCount++
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
})
|
||||
|
||||
// Wait for initial validation
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
// Click the validation icon
|
||||
await wrapper.find('.pi-check').trigger('click')
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(validationCount).toBe(2) // Once on mount, once on click
|
||||
})
|
||||
|
||||
it('prevents multiple simultaneous validations', async () => {
|
||||
let validationCount = 0
|
||||
const wrapper = mountComponent({
|
||||
modelValue: '',
|
||||
validateUrlFn: () => {
|
||||
validationCount++
|
||||
return new Promise(() => {
|
||||
// Never resolves, simulating perpetual loading state
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
wrapper.setProps({ modelValue: 'https://test.com' })
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
// Trigger multiple validations in quick succession
|
||||
wrapper.find('.pi-spinner').trigger('click')
|
||||
wrapper.find('.pi-spinner').trigger('click')
|
||||
wrapper.find('.pi-spinner').trigger('click')
|
||||
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(validationCount).toBe(1) // Only the initial validation should occur
|
||||
})
|
||||
})
|
||||
@@ -13,16 +13,11 @@
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
||||
if (
|
||||
settingStore.get('Comfy.Window.UnloadConfirmation') &&
|
||||
workflowStore.modifiedWorkflows.length > 0
|
||||
) {
|
||||
if (settingStore.get('Comfy.Window.UnloadConfirmation')) {
|
||||
event.preventDefault()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -28,10 +28,8 @@
|
||||
</template>
|
||||
<ReportIssuePanel
|
||||
v-if="sendReportOpen"
|
||||
:title="$t('issueReport.submitErrorReport')"
|
||||
error-type="graphExecutionError"
|
||||
:extra-fields="[stackTraceField]"
|
||||
:tags="{ exceptionMessage: props.error.exception_message }"
|
||||
/>
|
||||
<div class="action-container">
|
||||
<FindIssueButton
|
||||
@@ -90,10 +88,10 @@ const stackTraceField = computed<ReportField>(() => {
|
||||
label: t('issueReport.stackTrace'),
|
||||
value: 'StackTrace',
|
||||
optIn: true,
|
||||
getData: () => ({
|
||||
data: {
|
||||
nodeType: props.error.node_type,
|
||||
stackTrace: props.error.traceback?.join('\n')
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<div class="p-2 h-full" aria-labelledby="issue-report-title">
|
||||
<Panel
|
||||
:pt="{
|
||||
root: 'border-none',
|
||||
content: 'p-0'
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<header class="flex flex-col items-center w-full">
|
||||
<h2 id="issue-report-title" class="text-4xl">{{ title }}</h2>
|
||||
<span v-if="subtitle" class="text-muted mt-0">{{ subtitle }}</span>
|
||||
</header>
|
||||
</template>
|
||||
<ReportIssuePanel v-bind="panelProps" :pt="{ root: 'border-none' }" />
|
||||
</Panel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Panel from 'primevue/panel'
|
||||
|
||||
import ReportIssuePanel from '@/components/dialog/content/error/ReportIssuePanel.vue'
|
||||
import type { IssueReportPanelProps } from '@/types/issueReportTypes'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
subtitle?: string
|
||||
panelProps: IssueReportPanelProps
|
||||
}>()
|
||||
</script>
|
||||
@@ -2,15 +2,9 @@
|
||||
<NoResultsPlaceholder
|
||||
class="pb-0"
|
||||
icon="pi pi-exclamation-circle"
|
||||
:title="t('missingModelsDialog.missingModels')"
|
||||
:message="t('missingModelsDialog.missingModelsMessage')"
|
||||
title="Missing Models"
|
||||
message="When loading the graph, the following models were not found"
|
||||
/>
|
||||
<div class="flex gap-1 mb-4">
|
||||
<Checkbox v-model="doNotAskAgain" binary input-id="doNotAskAgain" />
|
||||
<label for="doNotAskAgain">{{
|
||||
t('missingModelsDialog.doNotAskAgain')
|
||||
}}</label>
|
||||
</div>
|
||||
<ListBox :options="missingModels" class="comfy-missing-models">
|
||||
<template #option="{ option }">
|
||||
<Suspense v-if="isElectron()">
|
||||
@@ -31,14 +25,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import ListBox from 'primevue/listbox'
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import FileDownload from '@/components/common/FileDownload.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
// TODO: Read this from server internal API rather than hardcoding here
|
||||
@@ -67,10 +58,6 @@ const props = defineProps<{
|
||||
paths: Record<string, string[]>
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const doNotAskAgain = ref(false)
|
||||
|
||||
const modelDownloads = ref<Record<string, ModelInfo>>({})
|
||||
const missingModels = computed(() => {
|
||||
return props.missingModels.map((model) => {
|
||||
@@ -120,12 +107,6 @@ const missingModels = computed(() => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (doNotAskAgain.value) {
|
||||
useSettingStore().set('Comfy.Workflow.ShowMissingModelsWarning', false)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,138 +1,81 @@
|
||||
<template>
|
||||
<Form
|
||||
v-slot="$form"
|
||||
@submit="submit"
|
||||
:resolver="zodResolver(issueReportSchema)"
|
||||
>
|
||||
<Panel :pt="$attrs.pt">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-bold">{{ title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-4">
|
||||
<Button
|
||||
v-tooltip="!submitted ? $t('g.reportIssueTooltip') : undefined"
|
||||
:label="submitted ? $t('g.reportSent') : $t('g.reportIssue')"
|
||||
:severity="submitted ? 'secondary' : 'primary'"
|
||||
:icon="submitted ? 'pi pi-check' : 'pi pi-send'"
|
||||
:disabled="submitted"
|
||||
type="submit"
|
||||
/>
|
||||
</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"
|
||||
>
|
||||
<Checkbox
|
||||
v-bind="$field"
|
||||
:inputId="field.value"
|
||||
:value="field.value"
|
||||
v-model="selection"
|
||||
/>
|
||||
<label :for="field.value">{{ field.label }}</label>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
<FormField class="mb-4" v-slot="$field" 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"
|
||||
:inputId="checkbox.value"
|
||||
:value="checkbox.value"
|
||||
v-model="contactPrefs"
|
||||
:disabled="
|
||||
$form.contactInfo?.error || !$form.contactInfo?.value
|
||||
"
|
||||
/>
|
||||
<label :for="checkbox.value">{{ checkbox.label }}</label>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
<Panel>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-bold">{{ $t('issueReport.submitErrorReport') }}</span>
|
||||
</div>
|
||||
</Panel>
|
||||
</Form>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
v-tooltip="$t('g.reportIssueTooltip')"
|
||||
:label="submitted ? $t('g.reportSent') : $t('g.reportIssue')"
|
||||
:severity="isButtonDisabled ? 'secondary' : 'primary'"
|
||||
:icon="icon"
|
||||
:disabled="isButtonDisabled"
|
||||
@click="reportIssue"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="p-4 mt-4 border border-round surface-border shadow-1">
|
||||
<CheckboxGroup
|
||||
v-model="selection"
|
||||
class="gap-4 mb-4"
|
||||
:checkboxes="reportCheckboxes"
|
||||
/>
|
||||
<div class="mb-4">
|
||||
<InputText
|
||||
v-model="contactInfo"
|
||||
class="w-full"
|
||||
:placeholder="$t('issueReport.provideEmail')"
|
||||
:maxlength="CONTACT_MAX_LEN"
|
||||
/>
|
||||
<CheckboxGroup
|
||||
v-model="contactPrefs"
|
||||
class="gap-3 mt-2"
|
||||
:checkboxes="contactCheckboxes"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<Textarea
|
||||
v-model="details"
|
||||
class="w-full"
|
||||
rows="4"
|
||||
:maxlength="DETAILS_MAX_LEN"
|
||||
:placeholder="$t('issueReport.provideAdditionalDetails')"
|
||||
:aria-label="$t('issueReport.provideAdditionalDetails')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Form, FormField, type FormSubmitEvent } from '@primevue/forms'
|
||||
// @ts-expect-error https://github.com/primefaces/primevue/issues/6722
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import type { CaptureContext, User } from '@sentry/core'
|
||||
import { captureMessage } from '@sentry/core'
|
||||
import _ from 'lodash'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import Button from 'primevue/button'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Message from 'primevue/message'
|
||||
import Panel from 'primevue/panel'
|
||||
import Textarea from 'primevue/textarea'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import CheckboxGroup from '@/components/common/CheckboxGroup.vue'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import {
|
||||
type IssueReportFormData,
|
||||
type ReportField,
|
||||
issueReportSchema
|
||||
} from '@/types/issueReportTypes'
|
||||
import type {
|
||||
DefaultField,
|
||||
IssueReportPanelProps
|
||||
} from '@/types/issueReportTypes'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
import type { DefaultField, ReportField } from '@/types/issueReportTypes'
|
||||
|
||||
const ISSUE_NAME = 'User reported issue'
|
||||
const DETAILS_MAX_LEN = 5_000
|
||||
const CONTACT_MAX_LEN = 320
|
||||
|
||||
const props = defineProps<IssueReportPanelProps>()
|
||||
const props = defineProps<{
|
||||
errorType: string
|
||||
defaultFields?: DefaultField[]
|
||||
extraFields?: ReportField[]
|
||||
}>()
|
||||
const { defaultFields = ['Workflow', 'Logs', 'SystemStats', 'Settings'] } =
|
||||
props
|
||||
|
||||
@@ -141,112 +84,114 @@ const toast = useToast()
|
||||
|
||||
const selection = ref<string[]>([])
|
||||
const contactPrefs = ref<string[]>([])
|
||||
const contactInfo = ref('')
|
||||
const details = ref('')
|
||||
const submitting = ref(false)
|
||||
const submitted = ref(false)
|
||||
|
||||
const followUp = computed(() => contactPrefs.value.includes('FollowUp'))
|
||||
const notifyResolve = computed(() => contactPrefs.value.includes('Resolution'))
|
||||
|
||||
const icon = computed(() => {
|
||||
if (submitting.value) return 'pi pi-spin pi-spinner'
|
||||
if (submitted.value) return 'pi pi-check'
|
||||
return 'pi pi-send'
|
||||
})
|
||||
const isFormEmpty = computed(() => !selection.value.length && !details.value)
|
||||
const isButtonDisabled = computed(
|
||||
() => submitted.value || submitting.value || isFormEmpty.value
|
||||
)
|
||||
|
||||
const contactCheckboxes = [
|
||||
{ label: t('issueReport.contactFollowUp'), value: 'followUp' },
|
||||
{ label: t('issueReport.notifyResolve'), value: 'notifyOnResolution' }
|
||||
{ label: t('issueReport.contactFollowUp'), value: 'FollowUp' },
|
||||
{ label: t('issueReport.notifyResolve'), value: 'Resolution' }
|
||||
]
|
||||
|
||||
const defaultFieldsConfig: ReportField[] = [
|
||||
{
|
||||
label: t('issueReport.systemStats'),
|
||||
value: 'SystemStats',
|
||||
getData: () => api.getSystemStats(),
|
||||
optIn: true
|
||||
},
|
||||
{
|
||||
label: t('g.workflow'),
|
||||
value: 'Workflow',
|
||||
getData: () => cloneDeep(app.graph.asSerialisable()),
|
||||
optIn: true
|
||||
},
|
||||
{
|
||||
label: t('g.logs'),
|
||||
value: 'Logs',
|
||||
getData: () => api.getLogs(),
|
||||
optIn: true
|
||||
},
|
||||
{
|
||||
label: t('g.settings'),
|
||||
value: 'Settings',
|
||||
getData: () => api.getSettings(),
|
||||
optIn: true
|
||||
}
|
||||
const defaultReportCheckboxes = [
|
||||
{ label: t('g.workflow'), value: 'Workflow' },
|
||||
{ label: t('g.logs'), value: 'Logs' },
|
||||
{ label: t('issueReport.systemStats'), value: 'SystemStats' },
|
||||
{ label: t('g.settings'), value: 'Settings' }
|
||||
]
|
||||
|
||||
const fields = computed(() => [
|
||||
...defaultFieldsConfig.filter(({ value }) =>
|
||||
const reportCheckboxes = computed(() => [
|
||||
...(props.extraFields?.map(({ label, value }) => ({ label, value })) ?? []),
|
||||
...defaultReportCheckboxes.filter(({ value }) =>
|
||||
defaultFields.includes(value as DefaultField)
|
||||
),
|
||||
...(props.extraFields ?? [])
|
||||
)
|
||||
])
|
||||
|
||||
const createUser = (formData: IssueReportFormData): User => ({
|
||||
email: formData.contactInfo || undefined
|
||||
})
|
||||
const getUserInfo = (): User => ({ email: contactInfo.value })
|
||||
|
||||
const createExtraData = async (formData: IssueReportFormData) => {
|
||||
const result = {}
|
||||
const isChecked = (fieldValue: string) => formData[fieldValue]
|
||||
const getLogs = async () =>
|
||||
selection.value.includes('Logs') ? api.getLogs() : null
|
||||
|
||||
await Promise.all(
|
||||
fields.value
|
||||
.filter((field) => !field.optIn || isChecked(field.value))
|
||||
.map(async (field) => {
|
||||
try {
|
||||
result[field.value] = await field.getData()
|
||||
} catch (error) {
|
||||
console.error(`Failed to collect ${field.value}:`, error)
|
||||
result[field.value] = { error: String(error) }
|
||||
}
|
||||
})
|
||||
)
|
||||
const getSystemStats = async () =>
|
||||
selection.value.includes('SystemStats') ? api.getSystemStats() : null
|
||||
|
||||
return result
|
||||
const getSettings = async () =>
|
||||
selection.value.includes('Settings') ? api.getSettings() : null
|
||||
|
||||
const getWorkflow = () =>
|
||||
selection.value.includes('Workflow')
|
||||
? cloneDeep(app.graph.asSerialisable())
|
||||
: null
|
||||
|
||||
const createDefaultFields = async () => {
|
||||
const [settings, systemStats, logs, workflow] = await Promise.all([
|
||||
getSettings(),
|
||||
getSystemStats(),
|
||||
getLogs(),
|
||||
getWorkflow()
|
||||
])
|
||||
return { settings, systemStats, logs, workflow }
|
||||
}
|
||||
|
||||
const createCaptureContext = async (
|
||||
formData: IssueReportFormData
|
||||
): Promise<CaptureContext> => {
|
||||
const createExtraFields = (): Record<string, unknown> | undefined => {
|
||||
if (!props.extraFields) return undefined
|
||||
|
||||
return props.extraFields
|
||||
.filter((field) => !field.optIn || selection.value.includes(field.value))
|
||||
.reduce((acc, field) => ({ ...acc, ...cloneDeep(field.data) }), {})
|
||||
}
|
||||
|
||||
const createFeedback = () => {
|
||||
return {
|
||||
user: createUser(formData),
|
||||
level: 'error',
|
||||
tags: {
|
||||
errorType: props.errorType,
|
||||
followUp: formData.contactInfo ? formData.followUp : false,
|
||||
notifyOnResolution: formData.contactInfo
|
||||
? formData.notifyOnResolution
|
||||
: false,
|
||||
isElectron: isElectron(),
|
||||
..._.mapValues(props.tags, (tag) => _.trim(tag).replace(/[\n\r\t]/g, ' '))
|
||||
},
|
||||
extra: {
|
||||
details: formData.details,
|
||||
...(await createExtraData(formData))
|
||||
details: details.value,
|
||||
contactPreferences: {
|
||||
followUp: followUp.value,
|
||||
notifyOnResolution: notifyResolve.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const submit = async (event: FormSubmitEvent) => {
|
||||
if (event.valid) {
|
||||
try {
|
||||
const captureContext = await createCaptureContext(event.values)
|
||||
captureMessage(ISSUE_NAME, captureContext)
|
||||
submitted.value = true
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('g.reportSent'),
|
||||
life: 3000
|
||||
})
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: error.message,
|
||||
life: 3000
|
||||
})
|
||||
const createCaptureContext = async (): Promise<CaptureContext> => {
|
||||
return {
|
||||
user: getUserInfo(),
|
||||
level: 'error',
|
||||
tags: {
|
||||
errorType: props.errorType
|
||||
},
|
||||
extra: {
|
||||
...createFeedback(),
|
||||
...(await createDefaultFields()),
|
||||
...createExtraFields()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const reportIssue = async () => {
|
||||
if (isButtonDisabled.value) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
captureMessage(ISSUE_NAME, await createCaptureContext())
|
||||
submitted.value = true
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('g.reportSent'),
|
||||
life: 3000
|
||||
})
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,51 +1,26 @@
|
||||
// @ts-strict-ignore
|
||||
import { Form } from '@primevue/forms'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import Button from 'primevue/button'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Panel from 'primevue/panel'
|
||||
import Textarea from 'primevue/textarea'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { createApp } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import CheckboxGroup from '@/components/common/CheckboxGroup.vue'
|
||||
import enMesages from '@/locales/en/main.json'
|
||||
import { IssueReportPanelProps } from '@/types/issueReportTypes'
|
||||
import { DefaultField, ReportField } from '@/types/issueReportTypes'
|
||||
|
||||
import ReportIssuePanel from '../ReportIssuePanel.vue'
|
||||
|
||||
const DEFAULT_FIELDS = ['Workflow', 'Logs', 'Settings', 'SystemStats']
|
||||
const CUSTOM_FIELDS = [
|
||||
{
|
||||
label: 'Custom Field',
|
||||
value: 'CustomField',
|
||||
optIn: true,
|
||||
getData: () => 'mock data'
|
||||
}
|
||||
]
|
||||
|
||||
async function getSubmittedContext() {
|
||||
const { captureMessage } = (await import('@sentry/core')) as any
|
||||
return captureMessage.mock.calls[0][1]
|
||||
}
|
||||
|
||||
async function submitForm(wrapper: any) {
|
||||
await wrapper.findComponent(Form).trigger('submit')
|
||||
return getSubmittedContext()
|
||||
}
|
||||
|
||||
async function findAndUpdateCheckbox(
|
||||
wrapper: any,
|
||||
value: string,
|
||||
checked = true
|
||||
) {
|
||||
const checkbox = wrapper
|
||||
.findAllComponents(Checkbox)
|
||||
.find((c: any) => c.props('value') === value)
|
||||
if (!checkbox) throw new Error(`Checkbox with value "${value}" not found`)
|
||||
|
||||
await checkbox.vm.$emit('update:modelValue', checked)
|
||||
return checkbox
|
||||
type ReportIssuePanelProps = {
|
||||
errorType: string
|
||||
defaultFields?: DefaultField[]
|
||||
extraFields?: ReportField[]
|
||||
}
|
||||
|
||||
const i18n = createI18n({
|
||||
@@ -82,64 +57,18 @@ vi.mock('@sentry/core', () => ({
|
||||
captureMessage: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@primevue/forms', () => ({
|
||||
Form: {
|
||||
name: 'Form',
|
||||
template:
|
||||
'<form @submit.prevent="onSubmit"><slot :values="formValues" /></form>',
|
||||
props: ['resolver'],
|
||||
data() {
|
||||
return {
|
||||
formValues: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
this.$emit('submit', {
|
||||
valid: true,
|
||||
values: this.formValues
|
||||
})
|
||||
},
|
||||
updateFieldValue(name: string, value: any) {
|
||||
this.formValues[name] = value
|
||||
}
|
||||
}
|
||||
},
|
||||
FormField: {
|
||||
name: 'FormField',
|
||||
template:
|
||||
'<div><slot :modelValue="modelValue" @update:modelValue="updateValue" /></div>',
|
||||
props: ['name'],
|
||||
data() {
|
||||
return {
|
||||
modelValue: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateValue(value) {
|
||||
this.modelValue = value
|
||||
let parent = this.$parent
|
||||
while (parent && parent.$options.name !== 'Form') {
|
||||
parent = parent.$parent
|
||||
}
|
||||
if (parent) {
|
||||
parent.updateFieldValue(this.name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
describe('ReportIssuePanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
beforeAll(() => {
|
||||
const app = createApp({})
|
||||
app.use(PrimeVue)
|
||||
})
|
||||
|
||||
const mountComponent = (props: IssueReportPanelProps, options = {}): any => {
|
||||
const mountComponent = (props: ReportIssuePanelProps, options = {}): any => {
|
||||
return mount(ReportIssuePanel, {
|
||||
global: {
|
||||
plugins: [PrimeVue, i18n],
|
||||
directives: { tooltip: Tooltip }
|
||||
plugins: [PrimeVue, createTestingPinia(), i18n],
|
||||
directives: { tooltip: Tooltip },
|
||||
components: { InputText, Button, Panel, Textarea, CheckboxGroup }
|
||||
},
|
||||
props,
|
||||
...options
|
||||
@@ -149,66 +78,44 @@ describe('ReportIssuePanel', () => {
|
||||
it('renders the panel with all required components', () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
expect(wrapper.find('.p-panel').exists()).toBe(true)
|
||||
expect(wrapper.findAllComponents(Checkbox).length).toBe(6)
|
||||
expect(wrapper.findAllComponents(CheckboxGroup).length).toBe(2)
|
||||
expect(wrapper.findComponent(InputText).exists()).toBe(true)
|
||||
expect(wrapper.findComponent(Textarea).exists()).toBe(true)
|
||||
expect(wrapper.findComponent(Button).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('updates selection when checkboxes are selected', async () => {
|
||||
const wrapper = mountComponent({
|
||||
errorType: 'Test Error'
|
||||
})
|
||||
|
||||
const checkboxes = wrapper.findAllComponents(Checkbox)
|
||||
|
||||
for (const field of DEFAULT_FIELDS) {
|
||||
const checkbox = checkboxes.find(
|
||||
(checkbox) => checkbox.props('value') === field
|
||||
)
|
||||
expect(checkbox).toBeDefined()
|
||||
|
||||
await checkbox?.vm.$emit('update:modelValue', [field])
|
||||
expect(wrapper.vm.selection).toContain(field)
|
||||
}
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
|
||||
await checkboxes?.setValue(['Workflow', 'Logs'])
|
||||
expect(wrapper.vm.selection).toEqual(['Workflow', 'Logs'])
|
||||
})
|
||||
|
||||
it('updates contactInfo when input is changed', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
const input = wrapper.findComponent(InputText)
|
||||
|
||||
await input.vm.$emit('update:modelValue', 'test@example.com')
|
||||
const context = await submitForm(wrapper)
|
||||
expect(context.user.email).toBe('test@example.com')
|
||||
await input.setValue('test@example.com')
|
||||
expect(wrapper.vm.contactInfo).toBe('test@example.com')
|
||||
})
|
||||
|
||||
it('updates additional details when textarea is changed', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
const textarea = wrapper.findComponent(Textarea)
|
||||
|
||||
await textarea.vm.$emit('update:modelValue', 'This is a test detail.')
|
||||
const context = await submitForm(wrapper)
|
||||
expect(context.extra.details).toBe('This is a test detail.')
|
||||
await textarea.setValue('This is a test detail.')
|
||||
expect(wrapper.vm.details).toBe('This is a test detail.')
|
||||
})
|
||||
|
||||
it('set contact preferences back to false if email is removed', async () => {
|
||||
it('updates contactPrefs when preferences are selected', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
const input = wrapper.findComponent(InputText)
|
||||
const preferences = wrapper.findAllComponents(CheckboxGroup).at(1)
|
||||
await preferences?.setValue(['FollowUp'])
|
||||
expect(wrapper.vm.contactPrefs).toEqual(['FollowUp'])
|
||||
})
|
||||
|
||||
// Set a valid email, enabling the contact preferences to be changed
|
||||
await input.vm.$emit('update:modelValue', 'name@example.com')
|
||||
|
||||
// Enable both contact preferences
|
||||
for (const pref of ['followUp', 'notifyOnResolution']) {
|
||||
await findAndUpdateCheckbox(wrapper, pref)
|
||||
}
|
||||
|
||||
// Change the email back to empty
|
||||
await input.vm.$emit('update:modelValue', '')
|
||||
const context = await submitForm(wrapper)
|
||||
|
||||
// Check that the contact preferences are back to false automatically
|
||||
expect(context.tags.followUp).toBe(false)
|
||||
expect(context.tags.notifyOnResolution).toBe(false)
|
||||
it('does not allow submission if the form is empty', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
await wrapper.vm.reportIssue()
|
||||
expect(wrapper.vm.submitted).toBe(false)
|
||||
})
|
||||
|
||||
it('renders with overridden default fields', () => {
|
||||
@@ -216,87 +123,83 @@ describe('ReportIssuePanel', () => {
|
||||
errorType: 'Test Error',
|
||||
defaultFields: ['Settings']
|
||||
})
|
||||
|
||||
// Filter out the contact preferences checkboxes
|
||||
const fieldCheckboxes = wrapper
|
||||
.findAllComponents(Checkbox)
|
||||
.filter(
|
||||
(checkbox) =>
|
||||
!['followUp', 'notifyOnResolution'].includes(checkbox.props('value'))
|
||||
)
|
||||
expect(fieldCheckboxes.length).toBe(1)
|
||||
expect(fieldCheckboxes.at(0)?.props('value')).toBe('Settings')
|
||||
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
|
||||
expect(checkboxes?.props('checkboxes')).toEqual([
|
||||
{ label: 'Settings', value: 'Settings' }
|
||||
])
|
||||
})
|
||||
|
||||
it('renders additional fields when extraFields prop is provided', () => {
|
||||
const wrapper = mountComponent({
|
||||
errorType: 'Test Error',
|
||||
extraFields: CUSTOM_FIELDS
|
||||
const extraFields = [
|
||||
{ label: 'Custom Field', value: 'CustomField', optIn: true, data: {} }
|
||||
]
|
||||
const wrapper = mountComponent({ errorType: 'Test Error', extraFields })
|
||||
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
|
||||
expect(checkboxes?.props('checkboxes')).toContainEqual({
|
||||
label: 'Custom Field',
|
||||
value: 'CustomField'
|
||||
})
|
||||
const customCheckbox = wrapper
|
||||
.findAllComponents(Checkbox)
|
||||
.find((checkbox) => checkbox.props('value') === 'CustomField')
|
||||
expect(customCheckbox).toBeDefined()
|
||||
})
|
||||
|
||||
it('allows custom fields to be selected', async () => {
|
||||
const wrapper = mountComponent({
|
||||
errorType: 'Test Error',
|
||||
extraFields: CUSTOM_FIELDS
|
||||
})
|
||||
|
||||
await findAndUpdateCheckbox(wrapper, 'CustomField')
|
||||
const context = await submitForm(wrapper)
|
||||
expect(context.extra.CustomField).toBe('mock data')
|
||||
})
|
||||
|
||||
it('does not submit unchecked fields', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
const textarea = wrapper.findComponent(Textarea)
|
||||
|
||||
// Set details but don't check any field checkboxes
|
||||
await textarea.vm.$emit(
|
||||
'update:modelValue',
|
||||
'Report with only text but no fields selected'
|
||||
)
|
||||
const context = await submitForm(wrapper)
|
||||
await textarea.setValue('Report with only text but no fields selected')
|
||||
await wrapper.vm.reportIssue()
|
||||
|
||||
// Verify none of the optional fields were included
|
||||
for (const field of DEFAULT_FIELDS) {
|
||||
expect(context.extra[field]).toBeUndefined()
|
||||
}
|
||||
const { captureMessage } = (await import('@sentry/core')) as any
|
||||
const captureContext = captureMessage.mock.calls[0][1]
|
||||
|
||||
expect(captureContext.extra.logs).toBeNull()
|
||||
expect(captureContext.extra.systemStats).toBeNull()
|
||||
expect(captureContext.extra.settings).toBeNull()
|
||||
expect(captureContext.extra.workflow).toBeNull()
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
checkbox: 'Logs',
|
||||
apiMethod: 'getLogs',
|
||||
expectedKey: 'Logs',
|
||||
expectedKey: 'logs',
|
||||
mockValue: 'mock logs'
|
||||
},
|
||||
{
|
||||
checkbox: 'SystemStats',
|
||||
apiMethod: 'getSystemStats',
|
||||
expectedKey: 'SystemStats',
|
||||
expectedKey: 'systemStats',
|
||||
mockValue: 'mock stats'
|
||||
},
|
||||
{
|
||||
checkbox: 'Settings',
|
||||
apiMethod: 'getSettings',
|
||||
expectedKey: 'Settings',
|
||||
expectedKey: 'settings',
|
||||
mockValue: 'mock settings'
|
||||
}
|
||||
])(
|
||||
'submits $checkbox data when checkbox is selected',
|
||||
'submits (%s) when the (%s) checkbox is selected',
|
||||
async ({ checkbox, apiMethod, expectedKey, mockValue }) => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
|
||||
const { api } = (await import('@/scripts/api')) as any
|
||||
vi.spyOn(api, apiMethod).mockResolvedValue(mockValue)
|
||||
|
||||
await findAndUpdateCheckbox(wrapper, checkbox)
|
||||
const context = await submitForm(wrapper)
|
||||
expect(context.extra[expectedKey]).toBe(mockValue)
|
||||
const { captureMessage } = await import('@sentry/core')
|
||||
|
||||
// Select the checkbox
|
||||
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
|
||||
await checkboxes?.vm.$emit('update:modelValue', [checkbox])
|
||||
|
||||
await wrapper.vm.reportIssue()
|
||||
expect(api[apiMethod]).toHaveBeenCalled()
|
||||
|
||||
// Verify the message includes the associated data
|
||||
expect(captureMessage).toHaveBeenCalledWith(
|
||||
'User reported issue',
|
||||
expect.objectContaining({
|
||||
extra: expect.objectContaining({ [expectedKey]: mockValue })
|
||||
})
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -304,12 +207,24 @@ describe('ReportIssuePanel', () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
|
||||
const { app } = (await import('@/scripts/app')) as any
|
||||
const { captureMessage } = await import('@sentry/core')
|
||||
|
||||
const mockWorkflow = { nodes: [], edges: [] }
|
||||
vi.spyOn(app.graph, 'asSerialisable').mockReturnValue(mockWorkflow)
|
||||
|
||||
await findAndUpdateCheckbox(wrapper, 'Workflow')
|
||||
const context = await submitForm(wrapper)
|
||||
// Select the "Workflow" checkbox
|
||||
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
|
||||
await checkboxes?.vm.$emit('update:modelValue', ['Workflow'])
|
||||
|
||||
expect(context.extra.Workflow).toEqual(mockWorkflow)
|
||||
await wrapper.vm.reportIssue()
|
||||
expect(app.graph.asSerialisable).toHaveBeenCalled()
|
||||
|
||||
// Verify the message includes the workflow
|
||||
expect(captureMessage).toHaveBeenCalledWith(
|
||||
'User reported issue',
|
||||
expect.objectContaining({
|
||||
extra: expect.objectContaining({ workflow: mockWorkflow })
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
optionValue="id"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-file-export"
|
||||
icon="pi pi-upload"
|
||||
text
|
||||
:title="$t('g.export')"
|
||||
@click="colorPaletteService.exportColorPalette(activePaletteId)"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-file-import"
|
||||
icon="pi pi-download"
|
||||
text
|
||||
:title="$t('g.import')"
|
||||
@click="importCustomPalette"
|
||||
|
||||
@@ -29,7 +29,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CanvasPointer, LGraphNode, LiteGraph } from '@comfyorg/litegraph'
|
||||
import {
|
||||
CanvasPointer,
|
||||
ContextMenu,
|
||||
DragAndScale,
|
||||
LGraph,
|
||||
LGraphBadge,
|
||||
LGraphCanvas,
|
||||
LGraphGroup,
|
||||
LGraphNode,
|
||||
LLink,
|
||||
LiteGraph
|
||||
} from '@comfyorg/litegraph'
|
||||
import { computed, onMounted, ref, watch, watchEffect } from 'vue'
|
||||
|
||||
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
|
||||
@@ -42,29 +53,37 @@ import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vu
|
||||
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
|
||||
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
|
||||
import { CORE_SETTINGS } from '@/constants/coreSettings'
|
||||
import { useCanvasDrop } from '@/hooks/canvasDropHooks'
|
||||
import { useGlobalLitegraph } from '@/hooks/litegraphHooks'
|
||||
import { useWorkflowPersistence } from '@/hooks/workflowPersistenceHooks'
|
||||
import { i18n } from '@/i18n'
|
||||
import { usePragmaticDroppable } from '@/hooks/dndHooks'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||
import { getStorageValue, setStorageValue } from '@/scripts/utils'
|
||||
import { IS_CONTROL_WIDGET, updateControlWidgetLabel } from '@/scripts/widgets'
|
||||
import { useColorPaletteService } from '@/services/colorPaletteService'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { ComfyModelDef } from '@/stores/modelStore'
|
||||
import {
|
||||
ModelNodeProvider,
|
||||
useModelToNodeStore
|
||||
} from '@/stores/modelToNodeStore'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
|
||||
const emit = defineEmits(['ready'])
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
const litegraphService = useLitegraphService()
|
||||
const settingStore = useSettingStore()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
const betaMenuEnabled = computed(
|
||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
@@ -76,6 +95,29 @@ const canvasMenuEnabled = computed(() =>
|
||||
)
|
||||
const tooltipEnabled = computed(() => settingStore.get('Comfy.EnableTooltips'))
|
||||
|
||||
const storedWorkflows = JSON.parse(
|
||||
getStorageValue('Comfy.OpenWorkflowsPaths') || '[]'
|
||||
)
|
||||
const storedActiveIndex = JSON.parse(
|
||||
getStorageValue('Comfy.ActiveWorkflowIndex') || '-1'
|
||||
)
|
||||
const openWorkflows = computed(() => workspaceStore?.workflow?.openWorkflows)
|
||||
const activeWorkflow = computed(() => workspaceStore?.workflow?.activeWorkflow)
|
||||
const restoreState = computed<{ paths: string[]; activeIndex: number }>(() => {
|
||||
if (!openWorkflows.value || !activeWorkflow.value) {
|
||||
return { paths: [], activeIndex: -1 }
|
||||
}
|
||||
|
||||
const paths = openWorkflows.value
|
||||
.filter((workflow) => workflow?.isPersisted && !workflow.isModified)
|
||||
.map((workflow) => workflow.path)
|
||||
const activeIndex = openWorkflows.value.findIndex(
|
||||
(workflow) => workflow.path === activeWorkflow.value?.path
|
||||
)
|
||||
|
||||
return { paths, activeIndex }
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
const canvasInfoEnabled = settingStore.get('Comfy.Graph.CanvasInfo')
|
||||
if (canvasStore.canvas) {
|
||||
@@ -227,36 +269,105 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
watchEffect(() => {
|
||||
LiteGraph.context_menu_scaling = settingStore.get(
|
||||
'LiteGraph.ContextMenu.Scaling'
|
||||
)
|
||||
})
|
||||
|
||||
const loadCustomNodesI18n = async () => {
|
||||
try {
|
||||
const i18nData = await api.getCustomNodesI18n()
|
||||
Object.entries(i18nData).forEach(([locale, message]) => {
|
||||
i18n.global.mergeLocaleMessage(locale, message)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to load custom nodes i18n', error)
|
||||
const workflowStore = useWorkflowStore()
|
||||
const persistCurrentWorkflow = () => {
|
||||
const workflow = JSON.stringify(comfyApp.serializeGraph())
|
||||
localStorage.setItem('workflow', workflow)
|
||||
if (api.clientId) {
|
||||
sessionStorage.setItem(`workflow:${api.clientId}`, workflow)
|
||||
}
|
||||
}
|
||||
|
||||
const comfyAppReady = ref(false)
|
||||
const workflowPersistence = useWorkflowPersistence()
|
||||
useCanvasDrop(canvasRef)
|
||||
watchEffect(() => {
|
||||
if (workflowStore.activeWorkflow) {
|
||||
const workflow = workflowStore.activeWorkflow
|
||||
setStorageValue('Comfy.PreviousWorkflow', workflow.key)
|
||||
// When the activeWorkflow changes, the graph has already been loaded.
|
||||
// Saving the current state of the graph to the localStorage.
|
||||
persistCurrentWorkflow()
|
||||
}
|
||||
})
|
||||
|
||||
api.addEventListener('graphChanged', persistCurrentWorkflow)
|
||||
|
||||
usePragmaticDroppable(() => canvasRef.value, {
|
||||
onDrop: (event) => {
|
||||
const loc = event.location.current.input
|
||||
const dndData = event.source.data
|
||||
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
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
|
||||
// is on the node (top left corner)
|
||||
const pos = comfyApp.clientPosToCanvasPos([
|
||||
loc.clientX - 20,
|
||||
loc.clientY
|
||||
])
|
||||
litegraphService.addNodeOnGraph(nodeDef, { pos })
|
||||
} else if (node.data instanceof ComfyModelDef) {
|
||||
const model = node.data
|
||||
const pos = comfyApp.clientPosToCanvasPos([loc.clientX, loc.clientY])
|
||||
const nodeAtPos = comfyApp.graph.getNodeOnPos(pos[0], pos[1])
|
||||
let targetProvider: ModelNodeProvider | null = null
|
||||
let targetGraphNode: LGraphNode | null = null
|
||||
if (nodeAtPos) {
|
||||
const providers = modelToNodeStore.getAllNodeProviders(
|
||||
model.directory
|
||||
)
|
||||
for (const provider of providers) {
|
||||
if (provider.nodeDef.name === nodeAtPos.comfyClass) {
|
||||
targetGraphNode = nodeAtPos
|
||||
targetProvider = provider
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!targetGraphNode) {
|
||||
const provider = modelToNodeStore.getNodeProvider(model.directory)
|
||||
if (provider) {
|
||||
targetGraphNode = litegraphService.addNodeOnGraph(
|
||||
provider.nodeDef,
|
||||
{
|
||||
pos
|
||||
}
|
||||
)
|
||||
targetProvider = provider
|
||||
}
|
||||
}
|
||||
if (targetGraphNode) {
|
||||
const widget = targetGraphNode.widgets.find(
|
||||
(widget) => widget.name === targetProvider.key
|
||||
)
|
||||
if (widget) {
|
||||
widget.value = model.file_name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const comfyAppReady = ref(false)
|
||||
onMounted(async () => {
|
||||
useGlobalLitegraph()
|
||||
// Backward compatible
|
||||
// Assign all properties of lg to window
|
||||
window['LiteGraph'] = LiteGraph
|
||||
window['LGraph'] = LGraph
|
||||
window['LLink'] = LLink
|
||||
window['LGraphNode'] = LGraphNode
|
||||
window['LGraphGroup'] = LGraphGroup
|
||||
window['DragAndScale'] = DragAndScale
|
||||
window['LGraphCanvas'] = LGraphCanvas
|
||||
window['ContextMenu'] = ContextMenu
|
||||
window['LGraphBadge'] = LGraphBadge
|
||||
|
||||
comfyApp.vueAppReady = true
|
||||
|
||||
workspaceStore.spinner = true
|
||||
// ChangeTracker needs to be initialized before setup, as it will overwrite
|
||||
// some listeners of litegraph canvas.
|
||||
ChangeTracker.init(comfyApp)
|
||||
await loadCustomNodesI18n()
|
||||
await settingStore.loadSettingValues()
|
||||
CORE_SETTINGS.forEach((setting) => {
|
||||
settingStore.addSetting(setting)
|
||||
@@ -276,9 +387,17 @@ onMounted(async () => {
|
||||
'Comfy.CustomColorPalettes'
|
||||
)
|
||||
|
||||
// Restore workflow and workflow tabs state from storage
|
||||
await workflowPersistence.restorePreviousWorkflow()
|
||||
workflowPersistence.restoreWorkflowTabsState()
|
||||
const isRestorable = storedWorkflows?.length > 0 && storedActiveIndex >= 0
|
||||
if (isRestorable)
|
||||
workflowStore.openWorkflowsInBackground({
|
||||
left: storedWorkflows.slice(0, storedActiveIndex),
|
||||
right: storedWorkflows.slice(storedActiveIndex)
|
||||
})
|
||||
|
||||
watch(restoreState, ({ paths, activeIndex }) => {
|
||||
setStorageValue('Comfy.OpenWorkflowsPaths', JSON.stringify(paths))
|
||||
setStorageValue('Comfy.ActiveWorkflowIndex', JSON.stringify(activeIndex))
|
||||
})
|
||||
|
||||
// Start watching for locale change after the initial value is loaded.
|
||||
watch(
|
||||
|
||||
@@ -72,6 +72,14 @@
|
||||
'install.settings.dataCollectionDialog.collect.userJourneyEvents'
|
||||
)
|
||||
}}
|
||||
<span
|
||||
class="pi pi-info-circle text-neutral-400"
|
||||
v-tooltip="
|
||||
$t(
|
||||
'install.settings.dataCollectionDialog.collect.userJourneyTooltip'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -108,16 +116,6 @@
|
||||
}}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="mt-4">
|
||||
<a
|
||||
href="https://comfy.org/privacy"
|
||||
target="_blank"
|
||||
class="text-blue-400 hover:text-blue-300 underline"
|
||||
>
|
||||
{{ $t('install.settings.dataCollectionDialog.viewFullPolicy') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
@@ -130,8 +128,8 @@ import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const autoUpdate = defineModel<boolean>('autoUpdate', { required: true })
|
||||
const allowMetrics = defineModel<boolean>('allowMetrics', { required: true })
|
||||
const autoUpdate = defineModel('autoUpdate', { required: true })
|
||||
const allowMetrics = defineModel('allowMetrics', { required: true })
|
||||
|
||||
const showMetricsInfo = () => {
|
||||
showDialog.value = true
|
||||
|
||||
@@ -159,8 +159,8 @@ const pickGpu = (value: typeof selected.value) => {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.p-tag {
|
||||
<style lang="postcss">
|
||||
:root {
|
||||
--p-tag-gap: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
<template>
|
||||
<Panel
|
||||
:header="$t('install.settings.mirrorSettings')"
|
||||
toggleable
|
||||
:collapsed="!showMirrorInputs"
|
||||
pt:root="bg-neutral-800 border-none w-[600px]"
|
||||
>
|
||||
<template
|
||||
v-for="([item, modelValue], index) in mirrors"
|
||||
:key="item.settingId + item.mirror"
|
||||
>
|
||||
<Divider v-if="index > 0" />
|
||||
|
||||
<MirrorItem
|
||||
:item="item"
|
||||
v-model="modelValue.value"
|
||||
@state-change="validationStates[index] = $event"
|
||||
/>
|
||||
</template>
|
||||
<template #icons>
|
||||
<i
|
||||
:class="{
|
||||
'pi pi-spin pi-spinner text-neutral-400':
|
||||
validationState === ValidationState.LOADING,
|
||||
'pi pi-check text-green-500':
|
||||
validationState === ValidationState.VALID,
|
||||
'pi pi-times text-red-500':
|
||||
validationState === ValidationState.INVALID
|
||||
}"
|
||||
v-tooltip="validationStateTooltip"
|
||||
/>
|
||||
</template>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
CUDA_TORCH_URL,
|
||||
NIGHTLY_CPU_TORCH_URL,
|
||||
TorchDeviceType
|
||||
} from '@comfyorg/comfyui-electron-types'
|
||||
import Divider from 'primevue/divider'
|
||||
import Panel from 'primevue/panel'
|
||||
import { ModelRef, computed, 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 { ValidationState, mergeValidationStates } from '@/utils/validationUtil'
|
||||
|
||||
const showMirrorInputs = ref(false)
|
||||
const { device } = defineProps<{ device: TorchDeviceType }>()
|
||||
const pythonMirror = defineModel<string>('pythonMirror', { required: true })
|
||||
const pypiMirror = defineModel<string>('pypiMirror', { required: true })
|
||||
const torchMirror = defineModel<string>('torchMirror', { required: true })
|
||||
|
||||
const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
|
||||
const settingId = 'Comfy-Desktop.UV.TorchInstallMirror'
|
||||
switch (device) {
|
||||
case 'mps':
|
||||
return {
|
||||
settingId,
|
||||
mirror: NIGHTLY_CPU_TORCH_URL,
|
||||
fallbackMirror: NIGHTLY_CPU_TORCH_URL
|
||||
}
|
||||
case 'nvidia':
|
||||
return {
|
||||
settingId,
|
||||
mirror: CUDA_TORCH_URL,
|
||||
fallbackMirror: CUDA_TORCH_URL
|
||||
}
|
||||
case 'cpu':
|
||||
default:
|
||||
return {
|
||||
settingId,
|
||||
mirror: PYPI_MIRROR.mirror,
|
||||
fallbackMirror: PYPI_MIRROR.fallbackMirror
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mirrors = computed<[UVMirror, ModelRef<string>][]>(() => [
|
||||
[PYTHON_MIRROR, pythonMirror],
|
||||
[PYPI_MIRROR, pypiMirror],
|
||||
[getTorchMirrorItem(device), torchMirror]
|
||||
])
|
||||
|
||||
const validationStates = ref<ValidationState[]>(
|
||||
mirrors.value.map(() => ValidationState.IDLE)
|
||||
)
|
||||
const validationState = computed(() => {
|
||||
return mergeValidationStates(validationStates.value)
|
||||
})
|
||||
const validationStateTooltip = computed(() => {
|
||||
switch (validationState.value) {
|
||||
case ValidationState.INVALID:
|
||||
return t('install.settings.mirrorsUnreachable')
|
||||
case ValidationState.VALID:
|
||||
return t('install.settings.mirrorsReachable')
|
||||
default:
|
||||
return t('install.settings.checkingMirrors')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,60 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="w-full">
|
||||
<h3 class="text-lg font-medium text-neutral-100">
|
||||
{{ $t(`settings.${normalizedSettingId}.name`) }}
|
||||
</h3>
|
||||
<p class="text-sm text-neutral-400 mt-1">
|
||||
{{ $t(`settings.${normalizedSettingId}.tooltip`) }}
|
||||
</p>
|
||||
</div>
|
||||
<UrlInput
|
||||
v-model="modelValue"
|
||||
:validate-url-fn="
|
||||
(mirror: string) =>
|
||||
checkMirrorReachable(mirror + (item.validationPathSuffix ?? ''))
|
||||
"
|
||||
@state-change="validationState = $event"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { UVMirror } from '@/constants/uvMirrors'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { checkMirrorReachable } from '@/utils/networkUtil'
|
||||
import { ValidationState } from '@/utils/validationUtil'
|
||||
|
||||
const { item } = defineProps<{
|
||||
item: UVMirror
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'state-change': [state: ValidationState]
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<string>('modelValue', { required: true })
|
||||
const validationState = ref<ValidationState>(ValidationState.IDLE)
|
||||
|
||||
const normalizedSettingId = computed(() => {
|
||||
return normalizeI18nKey(item.settingId)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
modelValue.value = item.mirror
|
||||
})
|
||||
|
||||
watch(validationState, (newState) => {
|
||||
emit('state-change', newState)
|
||||
|
||||
// Set fallback mirror if default mirror is invalid
|
||||
if (
|
||||
newState === ValidationState.INVALID &&
|
||||
modelValue.value === item.mirror
|
||||
) {
|
||||
modelValue.value = item.fallbackMirror
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,36 +0,0 @@
|
||||
<template>
|
||||
<Tag :icon :severity :value />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PrimeIcons } from '@primevue/core/api'
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
|
||||
// Properties
|
||||
const props = defineProps<{
|
||||
error: boolean
|
||||
refreshing?: boolean
|
||||
}>()
|
||||
|
||||
// Bindings
|
||||
const icon = computed(() => {
|
||||
if (props.refreshing) return PrimeIcons.QUESTION
|
||||
if (props.error) return PrimeIcons.TIMES
|
||||
return PrimeIcons.CHECK
|
||||
})
|
||||
|
||||
const severity = computed(() => {
|
||||
if (props.refreshing) return 'info'
|
||||
if (props.error) return 'danger'
|
||||
return 'success'
|
||||
})
|
||||
|
||||
const value = computed(() => {
|
||||
if (props.refreshing) return t('maintenance.refreshing')
|
||||
if (props.error) return t('g.error')
|
||||
return t('maintenance.OK')
|
||||
})
|
||||
</script>
|
||||
@@ -1,127 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="task-div max-w-48 min-h-52 grid relative"
|
||||
:class="{ 'opacity-75': isLoading }"
|
||||
>
|
||||
<Card
|
||||
class="max-w-48 relative h-full overflow-hidden"
|
||||
:class="{ 'opacity-65': runner.state !== 'error' }"
|
||||
v-bind="(({ onClick, ...rest }) => rest)($attrs)"
|
||||
>
|
||||
<template #header>
|
||||
<i
|
||||
v-if="runner.state === 'error'"
|
||||
class="pi pi-exclamation-triangle text-red-500 absolute m-2 top-0 -right-14 opacity-15"
|
||||
style="font-size: 10rem"
|
||||
/>
|
||||
<img
|
||||
v-if="task.headerImg"
|
||||
:src="task.headerImg"
|
||||
class="object-contain w-full h-full opacity-25 pt-4 px-4"
|
||||
/>
|
||||
</template>
|
||||
<template #title>{{ task.name }}</template>
|
||||
<template #content>{{ description }}</template>
|
||||
<template #footer>
|
||||
<div class="flex gap-4 mt-1">
|
||||
<Button
|
||||
:icon="task.button?.icon"
|
||||
:label="task.button?.text"
|
||||
class="w-full"
|
||||
raised
|
||||
icon-pos="right"
|
||||
@click="(event) => $emit('execute', event)"
|
||||
:loading="isExecuting"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<i
|
||||
v-if="!isLoading && runner.state === 'OK'"
|
||||
class="task-card-ok pi pi-check"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Card from 'primevue/card'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
|
||||
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
|
||||
import { useMinLoadingDurationRef } from '@/utils/refUtil'
|
||||
|
||||
const taskStore = useMaintenanceTaskStore()
|
||||
const runner = computed(() => taskStore.getRunner(props.task))
|
||||
|
||||
// Properties
|
||||
const props = defineProps<{
|
||||
task: MaintenanceTask
|
||||
}>()
|
||||
|
||||
// Events
|
||||
defineEmits<{
|
||||
execute: [event: MouseEvent]
|
||||
}>()
|
||||
|
||||
// Bindings
|
||||
const description = computed(() =>
|
||||
runner.value.state === 'error'
|
||||
? props.task.errorDescription ?? props.task.shortDescription
|
||||
: props.task.shortDescription
|
||||
)
|
||||
|
||||
// Use a minimum run time to ensure tasks "feel" like they have run
|
||||
const reactiveLoading = computed(() => runner.value.refreshing)
|
||||
const reactiveExecuting = computed(() => runner.value.executing)
|
||||
|
||||
const isLoading = useMinLoadingDurationRef(reactiveLoading, 250)
|
||||
const isExecuting = useMinLoadingDurationRef(reactiveExecuting, 250)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-card-ok {
|
||||
@apply text-green-500 absolute -right-4 -bottom-4 opacity-100 row-span-full col-span-full transition-opacity;
|
||||
|
||||
font-size: 4rem;
|
||||
text-shadow: 0.25rem 0 0.5rem black;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.p-card {
|
||||
@apply transition-opacity;
|
||||
|
||||
--p-card-background: var(--p-button-secondary-background);
|
||||
opacity: 0.9;
|
||||
|
||||
&.opacity-65 {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.p-card-header) {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
:deep(.p-card-body) {
|
||||
z-index: 1;
|
||||
flex-grow: 1;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.task-div {
|
||||
> i {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover > i {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,86 +0,0 @@
|
||||
<template>
|
||||
<tr
|
||||
class="border-neutral-700 border-solid border-y"
|
||||
:class="{
|
||||
'opacity-50': runner.resolved,
|
||||
'opacity-75': isLoading && runner.resolved
|
||||
}"
|
||||
>
|
||||
<td class="text-center w-16">
|
||||
<TaskListStatusIcon :state="runner.state" :loading="isLoading" />
|
||||
</td>
|
||||
<td>
|
||||
<p class="inline-block">{{ task.name }}</p>
|
||||
<Button
|
||||
class="inline-block mx-2"
|
||||
type="button"
|
||||
:icon="PrimeIcons.INFO_CIRCLE"
|
||||
severity="secondary"
|
||||
:text="true"
|
||||
@click="toggle"
|
||||
/>
|
||||
|
||||
<Popover ref="infoPopover" class="block m-1 max-w-64 min-w-32">
|
||||
<span class="whitespace-pre-line">{{ task.description }}</span>
|
||||
</Popover>
|
||||
</td>
|
||||
<td class="text-right px-4">
|
||||
<Button
|
||||
:icon="task.button?.icon"
|
||||
:label="task.button?.text"
|
||||
:severity
|
||||
icon-pos="right"
|
||||
@click="(event) => $emit('execute', event)"
|
||||
:loading="isExecuting"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PrimeIcons } from '@primevue/core/api'
|
||||
import Button from 'primevue/button'
|
||||
import Popover from 'primevue/popover'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
|
||||
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
|
||||
import { VueSeverity } from '@/types/primeVueTypes'
|
||||
import { useMinLoadingDurationRef } from '@/utils/refUtil'
|
||||
|
||||
import TaskListStatusIcon from './TaskListStatusIcon.vue'
|
||||
|
||||
const taskStore = useMaintenanceTaskStore()
|
||||
const runner = computed(() => taskStore.getRunner(props.task))
|
||||
|
||||
// Properties
|
||||
const props = defineProps<{
|
||||
task: MaintenanceTask
|
||||
}>()
|
||||
|
||||
// Events
|
||||
defineEmits<{
|
||||
execute: [event: MouseEvent]
|
||||
}>()
|
||||
|
||||
// Binding
|
||||
const severity = computed<VueSeverity>(() =>
|
||||
runner.value.state === 'error' || runner.value.state === 'warning'
|
||||
? 'primary'
|
||||
: 'secondary'
|
||||
)
|
||||
|
||||
// Use a minimum run time to ensure tasks "feel" like they have run
|
||||
const reactiveLoading = computed(() => runner.value.refreshing)
|
||||
const reactiveExecuting = computed(() => runner.value.executing)
|
||||
|
||||
const isLoading = useMinLoadingDurationRef(reactiveLoading, 250)
|
||||
const isExecuting = useMinLoadingDurationRef(reactiveExecuting, 250)
|
||||
|
||||
// Popover
|
||||
const infoPopover = ref()
|
||||
|
||||
const toggle = (event: Event) => {
|
||||
infoPopover.value.toggle(event)
|
||||
}
|
||||
</script>
|
||||
@@ -1,115 +0,0 @@
|
||||
<template>
|
||||
<!-- Tasks -->
|
||||
<section class="my-4">
|
||||
<template v-if="filter.tasks.length === 0">
|
||||
<!-- Empty filter -->
|
||||
<Divider />
|
||||
<p class="text-neutral-400 w-full text-center">
|
||||
{{ $t('maintenance.allOk') }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<!-- Display: List -->
|
||||
<table
|
||||
v-if="displayAsList === PrimeIcons.LIST"
|
||||
class="w-full border-collapse border-hidden"
|
||||
>
|
||||
<TaskListItem
|
||||
v-for="task in filter.tasks"
|
||||
:key="task.id"
|
||||
:task
|
||||
@execute="(event) => confirmButton(event, task)"
|
||||
/>
|
||||
</table>
|
||||
|
||||
<!-- Display: Cards -->
|
||||
<template v-else>
|
||||
<div class="flex flex-wrap justify-evenly gap-8 pad-y my-4">
|
||||
<TaskCard
|
||||
v-for="task in filter.tasks"
|
||||
:key="task.id"
|
||||
:task
|
||||
@execute="(event) => confirmButton(event, task)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<ConfirmPopup />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PrimeIcons } from '@primevue/core/api'
|
||||
import { useConfirm, useToast } from 'primevue'
|
||||
import ConfirmPopup from 'primevue/confirmpopup'
|
||||
import Divider from 'primevue/divider'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
|
||||
import type {
|
||||
MaintenanceFilter,
|
||||
MaintenanceTask
|
||||
} from '@/types/desktop/maintenanceTypes'
|
||||
|
||||
import TaskCard from './TaskCard.vue'
|
||||
import TaskListItem from './TaskListItem.vue'
|
||||
|
||||
const toast = useToast()
|
||||
const confirm = useConfirm()
|
||||
const taskStore = useMaintenanceTaskStore()
|
||||
|
||||
// Properties
|
||||
const props = defineProps<{
|
||||
displayAsList: string
|
||||
filter: MaintenanceFilter
|
||||
isRefreshing: boolean
|
||||
}>()
|
||||
|
||||
const executeTask = async (task: MaintenanceTask) => {
|
||||
let message: string | undefined
|
||||
|
||||
try {
|
||||
// Success
|
||||
if ((await taskStore.execute(task)) === true) return
|
||||
|
||||
message = t('maintenance.error.taskFailed')
|
||||
} catch (error) {
|
||||
message = (error as Error)?.message
|
||||
}
|
||||
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('maintenance.error.toastTitle'),
|
||||
detail: message ?? t('maintenance.error.defaultDescription'),
|
||||
life: 10_000
|
||||
})
|
||||
}
|
||||
|
||||
// Commands
|
||||
const confirmButton = async (event: MouseEvent, task: MaintenanceTask) => {
|
||||
if (!task.requireConfirm) {
|
||||
await executeTask(task)
|
||||
return
|
||||
}
|
||||
|
||||
confirm.require({
|
||||
target: event.currentTarget as HTMLElement,
|
||||
message: task.confirmText ?? t('maintenance.confirmTitle'),
|
||||
icon: 'pi pi-exclamation-circle',
|
||||
rejectProps: {
|
||||
label: t('g.cancel'),
|
||||
severity: 'secondary',
|
||||
outlined: true
|
||||
},
|
||||
acceptProps: {
|
||||
label: task.button?.text ?? t('g.save'),
|
||||
severity: task.severity ?? 'primary'
|
||||
},
|
||||
// TODO: Not awaited.
|
||||
accept: async () => {
|
||||
await executeTask(task)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<ProgressSpinner v-if="!state || loading" class="h-8 w-8" />
|
||||
<template v-else>
|
||||
<i :class="cssClasses" v-tooltip.top="{ value: tooltip, showDelay: 250 }" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PrimeIcons } from '@primevue/core/api'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { MaybeRef, computed } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
|
||||
// Properties
|
||||
const tooltip = computed(() => {
|
||||
if (props.state === 'error') {
|
||||
return t('g.error')
|
||||
} else if (props.state === 'OK') {
|
||||
return t('maintenance.OK')
|
||||
} else {
|
||||
return t('maintenance.Skipped')
|
||||
}
|
||||
})
|
||||
|
||||
const cssClasses = computed(() => {
|
||||
let classes: string
|
||||
if (props.state === 'error') {
|
||||
classes = `${PrimeIcons.EXCLAMATION_TRIANGLE} text-red-500`
|
||||
} else if (props.state === 'OK') {
|
||||
classes = `${PrimeIcons.CHECK} text-green-500`
|
||||
} else {
|
||||
classes = PrimeIcons.MINUS
|
||||
}
|
||||
|
||||
return `text-3xl pi ${classes}`
|
||||
})
|
||||
|
||||
// Model
|
||||
const props = defineProps<{
|
||||
state: 'warning' | 'error' | 'resolved' | 'OK' | 'skipped' | undefined
|
||||
loading?: MaybeRef<boolean>
|
||||
}>()
|
||||
</script>
|
||||
@@ -6,27 +6,11 @@ export default {
|
||||
name: 'AutoCompletePlus',
|
||||
extends: AutoComplete,
|
||||
emits: ['focused-option-changed'],
|
||||
data() {
|
||||
return {
|
||||
// Flag to determine if IME is active
|
||||
isComposing: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (typeof AutoComplete.mounted === 'function') {
|
||||
AutoComplete.mounted.call(this)
|
||||
}
|
||||
|
||||
// Retrieve the actual <input> element and attach IME events
|
||||
const inputEl = this.$el.querySelector('input')
|
||||
if (inputEl) {
|
||||
inputEl.addEventListener('compositionstart', () => {
|
||||
this.isComposing = true
|
||||
})
|
||||
inputEl.addEventListener('compositionend', () => {
|
||||
this.isComposing = false
|
||||
})
|
||||
}
|
||||
// Add a watcher on the focusedOptionIndex property
|
||||
this.$watch(
|
||||
() => this.focusedOptionIndex,
|
||||
@@ -35,18 +19,6 @@ export default {
|
||||
this.$emit('focused-option-changed', newVal)
|
||||
}
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
// Override onKeyDown to block Enter when IME is active
|
||||
onKeyDown(event) {
|
||||
if (event.key === 'Enter' && this.isComposing) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
|
||||
AutoComplete.methods.onKeyDown.call(this, event)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -60,9 +60,8 @@
|
||||
<!-- FilterAndValue -->
|
||||
<template v-slot:chip="{ value }">
|
||||
<SearchFilterChip
|
||||
v-if="Array.isArray(value) && value.length === 2"
|
||||
:key="`${value[0].id}-${value[1]}`"
|
||||
@remove="onRemoveFilter($event, value as FilterAndValue)"
|
||||
@remove="onRemoveFilter($event, value)"
|
||||
:text="value[1]"
|
||||
:badge="value[0].invokeSequence.toUpperCase()"
|
||||
:badge-class="value[0].invokeSequence + '-badge'"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<teleport :to="teleportTarget">
|
||||
<nav class="side-tool-bar-container" :class="{ 'small-sidebar': isSmall }">
|
||||
<nav :class="'side-tool-bar-container' + (isSmall ? ' small-sidebar' : '')">
|
||||
<SidebarIcon
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
@@ -69,6 +69,17 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--sidebar-width: 4rem;
|
||||
--sidebar-icon-size: 1.5rem;
|
||||
}
|
||||
:root .small-sidebar {
|
||||
--sidebar-width: 2.5rem;
|
||||
--sidebar-icon-size: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.side-tool-bar-container {
|
||||
display: flex;
|
||||
@@ -83,14 +94,6 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
|
||||
background-color: var(--comfy-menu-secondary-bg);
|
||||
color: var(--fg-color);
|
||||
box-shadow: var(--bar-shadow);
|
||||
|
||||
--sidebar-width: 4rem;
|
||||
--sidebar-icon-size: 1.5rem;
|
||||
}
|
||||
|
||||
.side-tool-bar-container.small-sidebar {
|
||||
--sidebar-width: 2.5rem;
|
||||
--sidebar-icon-size: 1rem;
|
||||
}
|
||||
|
||||
.side-tool-bar-end {
|
||||
|
||||
@@ -20,6 +20,14 @@
|
||||
@click="alphabeticalSort = !alphabeticalSort"
|
||||
v-tooltip.bottom="$t('sideToolbar.nodeLibraryTab.sortOrder')"
|
||||
/>
|
||||
<Button
|
||||
class="grouping-button"
|
||||
:icon="groupBySource ? 'pi pi-list' : 'pi pi-list-check'"
|
||||
text
|
||||
severity="secondary"
|
||||
@click="groupBySource = !groupBySource"
|
||||
v-tooltip.bottom="$t('sideToolbar.nodeLibraryTab.groupingType')"
|
||||
/>
|
||||
</template>
|
||||
<template #header>
|
||||
<SearchBox
|
||||
@@ -66,12 +74,11 @@ import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import Popover from 'primevue/popover'
|
||||
import type { TreeNode } from 'primevue/treenode'
|
||||
import { Ref, computed, h, nextTick, ref, render } from 'vue'
|
||||
import { Ref, computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import { SearchFilter } from '@/components/common/SearchFilterChip.vue'
|
||||
import TreeExplorer from '@/components/common/TreeExplorer.vue'
|
||||
import NodePreview from '@/components/node/NodePreview.vue'
|
||||
import NodeSearchFilter from '@/components/searchbox/NodeSearchFilter.vue'
|
||||
import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue'
|
||||
import NodeTreeLeaf from '@/components/sidebar/tabs/nodeLibrary/NodeTreeLeaf.vue'
|
||||
@@ -102,6 +109,21 @@ const nodeBookmarkTreeExplorerRef = ref<InstanceType<
|
||||
> | null>(null)
|
||||
const searchFilter = ref(null)
|
||||
const alphabeticalSort = ref(false)
|
||||
const groupBySource = ref(false)
|
||||
|
||||
const createSourceKey = (nodeDef: ComfyNodeDefImpl) => {
|
||||
const sourcePath = nodeDef.python_module.split('.')
|
||||
const pathWithoutCategory = nodeDef.nodePath.split('/').slice(1)
|
||||
return [...sourcePath, ...pathWithoutCategory]
|
||||
}
|
||||
|
||||
watch(groupBySource, (newValue) => {
|
||||
if (newValue) {
|
||||
nodeDefStore.setKeyFunction(createSourceKey)
|
||||
} else {
|
||||
nodeDefStore.setKeyFunction(null)
|
||||
}
|
||||
})
|
||||
|
||||
const searchQuery = ref<string>('')
|
||||
|
||||
@@ -126,13 +148,6 @@ const renderedRoot = computed<TreeExplorerNode<ComfyNodeDefImpl>>(() => {
|
||||
},
|
||||
children,
|
||||
draggable: node.leaf,
|
||||
renderDragPreview: (node, container) => {
|
||||
const vnode = h(NodePreview, { nodeDef: node.data })
|
||||
render(vnode, container)
|
||||
return () => {
|
||||
render(null, container)
|
||||
}
|
||||
},
|
||||
handleClick: (
|
||||
node: RenderedTreeExplorerNode<ComfyNodeDefImpl>,
|
||||
e: MouseEvent
|
||||
@@ -202,4 +217,9 @@ const onRemoveFilter = (filterAndValue) => {
|
||||
}
|
||||
handleSearch(searchQuery.value)
|
||||
}
|
||||
|
||||
// This can be added if the persistent state is not desirable:
|
||||
// onBeforeUnmount(() => {
|
||||
// nodeDefStore.setKeyFunction(null)
|
||||
// })
|
||||
</script>
|
||||
|
||||
@@ -99,7 +99,7 @@ import type { MenuItem } from 'primevue/menuitem'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { useConfirm } from 'primevue/useconfirm'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, ref, shallowRef, watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
@@ -194,6 +194,10 @@ const confirmRemoveAll = (event: Event) => {
|
||||
})
|
||||
}
|
||||
|
||||
const onStatus = async () => {
|
||||
await queueStore.update()
|
||||
}
|
||||
|
||||
const menu = ref(null)
|
||||
const menuTargetTask = ref<TaskItemImpl | null>(null)
|
||||
const menuTargetNode = ref<ComfyNode | null>(null)
|
||||
@@ -263,4 +267,13 @@ watch(allTasks, () => {
|
||||
const newIndex = galleryActiveIndex.value + lengthChange
|
||||
galleryActiveIndex.value = Math.max(0, newIndex)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
api.addEventListener('status', onStatus)
|
||||
queueStore.update()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
api.removeEventListener('status', onStatus)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="comfy-vue-side-bar-container flex flex-col h-full group/sidebar-tab"
|
||||
class="comfy-vue-side-bar-container flex flex-col h-full"
|
||||
:class="props.class"
|
||||
>
|
||||
<div class="comfy-vue-side-bar-header">
|
||||
@@ -11,11 +11,7 @@
|
||||
</span>
|
||||
</template>
|
||||
<template #end>
|
||||
<div
|
||||
class="flex flex-row motion-safe:w-0 motion-safe:opacity-0 motion-safe:group-hover/sidebar-tab:w-auto motion-safe:group-hover/sidebar-tab:opacity-100 motion-safe:group-focus-within/sidebar-tab:w-auto motion-safe:group-focus-within/sidebar-tab:opacity-100 touch:w-auto touch:opacity-100 transition-all duration-200"
|
||||
>
|
||||
<slot name="tool-buttons"></slot>
|
||||
</div>
|
||||
<slot name="tool-buttons"></slot>
|
||||
</template>
|
||||
</Toolbar>
|
||||
<slot name="header"></slot>
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<Button
|
||||
class="browse-templates-button"
|
||||
icon="pi pi-th-large"
|
||||
severity="secondary"
|
||||
v-tooltip.bottom="$t('sideToolbar.browseTemplates')"
|
||||
text
|
||||
@click="() => commandStore.execute('Comfy.BrowseTemplates')"
|
||||
@@ -15,7 +14,6 @@
|
||||
<Button
|
||||
class="open-workflow-button"
|
||||
icon="pi pi-folder-open"
|
||||
severity="secondary"
|
||||
v-tooltip.bottom="$t('sideToolbar.openWorkflow')"
|
||||
text
|
||||
@click="() => commandStore.execute('Comfy.OpenWorkflow')"
|
||||
@@ -23,7 +21,6 @@
|
||||
<Button
|
||||
class="new-blank-workflow-button"
|
||||
icon="pi pi-plus"
|
||||
severity="secondary"
|
||||
v-tooltip.bottom="$t('sideToolbar.newBlankWorkflow')"
|
||||
@click="() => commandStore.execute('Comfy.NewBlankWorkflow')"
|
||||
text
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
<!-- Virtual top menu for native window (drag handle) -->
|
||||
<div
|
||||
v-show="isNativeWindow() && !showTopMenu"
|
||||
v-show="isNativeWindow && !showTopMenu"
|
||||
class="fixed top-0 left-0 app-drag w-full h-[var(--comfy-topbar-height)]"
|
||||
/>
|
||||
</template>
|
||||
@@ -50,12 +50,7 @@ import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import {
|
||||
electronAPI,
|
||||
isElectron,
|
||||
isNativeWindow,
|
||||
showNativeMenu
|
||||
} from '@/utils/envUtil'
|
||||
import { electronAPI, isElectron, showNativeMenu } from '@/utils/envUtil'
|
||||
|
||||
const workspaceState = useWorkspaceStore()
|
||||
const settingStore = useSettingStore()
|
||||
@@ -69,6 +64,10 @@ const teleportTarget = computed(() =>
|
||||
? '.comfyui-body-top'
|
||||
: '.comfyui-body-bottom'
|
||||
)
|
||||
const isNativeWindow = computed(
|
||||
() =>
|
||||
isElectron() && settingStore.get('Comfy-Desktop.WindowStyle') === 'custom'
|
||||
)
|
||||
const showTopMenu = computed(
|
||||
() => betaMenuEnabled.value && !workspaceState.focusMode
|
||||
)
|
||||
|
||||
@@ -19,9 +19,8 @@ export const CORE_MENU_COMMANDS = [
|
||||
[
|
||||
'Comfy.Help.OpenComfyUIIssues',
|
||||
'Comfy.Help.OpenComfyUIDocs',
|
||||
'Comfy.Help.OpenComfyOrgDiscord',
|
||||
'Comfy.Help.OpenComfyUIForum'
|
||||
'Comfy.Help.OpenComfyOrgDiscord'
|
||||
]
|
||||
],
|
||||
[['Help'], ['Comfy.Help.AboutComfyUI', 'Comfy.Feedback']]
|
||||
[['Help'], ['Comfy.Help.AboutComfyUI']]
|
||||
]
|
||||
|
||||
@@ -157,7 +157,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
id: 'Comfy.Workflow.ShowMissingModelsWarning',
|
||||
name: 'Show missing models warning',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
defaultValue: false,
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
@@ -230,8 +230,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
id: 'Comfy.Window.UnloadConfirmation',
|
||||
name: 'Show confirmation when closing window',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
versionModified: '1.7.12'
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'Comfy.TreeExplorer.ItemPadding',
|
||||
@@ -290,7 +289,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
name: 'Node ID badge mode',
|
||||
type: 'combo',
|
||||
options: [NodeBadgeMode.None, NodeBadgeMode.ShowAll],
|
||||
defaultValue: NodeBadgeMode.None
|
||||
defaultValue: NodeBadgeMode.ShowAll
|
||||
},
|
||||
{
|
||||
id: 'Comfy.NodeBadge.NodeLifeCycleBadgeMode',
|
||||
@@ -708,19 +707,5 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
defaultValue: 'after',
|
||||
options: ['before', 'after'],
|
||||
versionModified: '1.6.10'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.TutorialCompleted',
|
||||
name: 'Tutorial completed',
|
||||
type: 'hidden',
|
||||
defaultValue: false,
|
||||
versionAdded: '1.8.7'
|
||||
},
|
||||
{
|
||||
id: 'LiteGraph.ContextMenu.Scaling',
|
||||
name: 'Scale node combo widget menus (lists) when zoomed in',
|
||||
defaultValue: false,
|
||||
type: 'boolean',
|
||||
versionAdded: '1.8.8'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
import { PrimeIcons } from '@primevue/core'
|
||||
|
||||
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
|
||||
const electron = electronAPI()
|
||||
|
||||
const openUrl = (url: string) => {
|
||||
window.open(url, '_blank')
|
||||
return true
|
||||
}
|
||||
|
||||
export const DESKTOP_MAINTENANCE_TASKS: Readonly<MaintenanceTask>[] = [
|
||||
{
|
||||
id: 'basePath',
|
||||
execute: async () => await electron.setBasePath(),
|
||||
name: 'Base path',
|
||||
shortDescription: 'Change the application base path.',
|
||||
errorDescription: 'Unable to open the base path. Please select a new one.',
|
||||
description:
|
||||
'The base path is the default location where ComfyUI stores data. It is the location fo the python environment, and may also contain models, custom nodes, and other extensions.',
|
||||
isInstallationFix: true,
|
||||
button: {
|
||||
icon: PrimeIcons.QUESTION,
|
||||
text: 'Select'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'git',
|
||||
headerImg: '/assets/images/Git-Logo-White.svg',
|
||||
execute: () => openUrl('https://git-scm.com/downloads/'),
|
||||
name: 'Download git',
|
||||
shortDescription: 'Open the git download page.',
|
||||
errorDescription:
|
||||
'Git is missing. Please download and install git, then restart ComfyUI Desktop.',
|
||||
description:
|
||||
'Git is required to download and manage custom nodes and other extensions. This task opens the download page in your default browser, where you can download the latest version of git. Once you have installed git, please restart ComfyUI Desktop.',
|
||||
button: {
|
||||
icon: PrimeIcons.EXTERNAL_LINK,
|
||||
text: 'Download'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'vcRedist',
|
||||
execute: () => openUrl('https://aka.ms/vs/17/release/vc_redist.x64.exe'),
|
||||
name: 'Download VC++ Redist',
|
||||
shortDescription: 'Download the latest VC++ Redistributable runtime.',
|
||||
description:
|
||||
'The Visual C++ runtime libraries are required to run ComfyUI. You will need to download and install this file.',
|
||||
button: {
|
||||
icon: PrimeIcons.EXTERNAL_LINK,
|
||||
text: 'Download'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'reinstall',
|
||||
severity: 'danger',
|
||||
requireConfirm: true,
|
||||
execute: async () => {
|
||||
await electron.reinstall()
|
||||
return true
|
||||
},
|
||||
name: 'Reinstall ComfyUI',
|
||||
shortDescription:
|
||||
'Deletes the desktop app config and load the welcome screen.',
|
||||
description:
|
||||
'Delete the desktop app config, restart the app, and load the installation screen.',
|
||||
confirmText: 'Delete all saved config and reinstall?',
|
||||
button: {
|
||||
icon: PrimeIcons.EXCLAMATION_TRIANGLE,
|
||||
text: 'Reinstall'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'pythonPackages',
|
||||
requireConfirm: true,
|
||||
execute: async () => {
|
||||
try {
|
||||
await electron.uv.installRequirements()
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
name: 'Install python packages',
|
||||
shortDescription:
|
||||
'Installs the base python packages required to run ComfyUI.',
|
||||
errorDescription:
|
||||
'Python packages that are required to run ComfyUI are not installed.',
|
||||
description:
|
||||
'This will install the python packages required to run ComfyUI. This includes torch, torchvision, and other dependencies.',
|
||||
usesTerminal: true,
|
||||
isInstallationFix: true,
|
||||
button: {
|
||||
icon: PrimeIcons.DOWNLOAD,
|
||||
text: 'Install'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'uv',
|
||||
execute: () =>
|
||||
openUrl('https://docs.astral.sh/uv/getting-started/installation/'),
|
||||
name: 'uv executable',
|
||||
shortDescription: 'uv installs and maintains the python environment.',
|
||||
description:
|
||||
"This will open the download page for Astral's uv tool. uv is used to install python and manage python packages.",
|
||||
button: {
|
||||
icon: 'pi pi-asterisk',
|
||||
text: 'Download'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'uvCache',
|
||||
severity: 'danger',
|
||||
requireConfirm: true,
|
||||
execute: async () => await electron.uv.clearCache(),
|
||||
name: 'uv cache',
|
||||
shortDescription: 'Remove the Astral uv cache of python packages.',
|
||||
description:
|
||||
'This will remove the uv cache directory and its contents. All downloaded python packages will need to be downloaded again.',
|
||||
confirmText: 'Delete uv cache of python packages?',
|
||||
isInstallationFix: true,
|
||||
button: {
|
||||
icon: PrimeIcons.TRASH,
|
||||
text: 'Clear cache'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'venvDirectory',
|
||||
severity: 'danger',
|
||||
requireConfirm: true,
|
||||
execute: async () => await electron.uv.resetVenv(),
|
||||
name: 'Reset virtual environment',
|
||||
shortDescription:
|
||||
'Remove and recreate the .venv directory. This removes all python packages.',
|
||||
description:
|
||||
'The python environment is where ComfyUI installs python and python packages. It is used to run the ComfyUI server.',
|
||||
confirmText: 'Delete the .venv directory?',
|
||||
usesTerminal: true,
|
||||
isInstallationFix: true,
|
||||
button: {
|
||||
icon: PrimeIcons.FOLDER,
|
||||
text: 'Recreate'
|
||||
}
|
||||
}
|
||||
] as const
|
||||
@@ -1,34 +0,0 @@
|
||||
export interface UVMirror {
|
||||
/**
|
||||
* The setting id defined for the mirror.
|
||||
*/
|
||||
settingId: string
|
||||
/**
|
||||
* The default mirror to use.
|
||||
*/
|
||||
mirror: string
|
||||
/**
|
||||
* The fallback mirror to use.
|
||||
*/
|
||||
fallbackMirror: string
|
||||
/**
|
||||
* The path suffix to validate the mirror is reachable.
|
||||
*/
|
||||
validationPathSuffix?: string
|
||||
}
|
||||
|
||||
export const PYTHON_MIRROR: UVMirror = {
|
||||
settingId: 'Comfy-Desktop.UV.PythonInstallMirror',
|
||||
mirror:
|
||||
'https://github.com/astral-sh/python-build-standalone/releases/download',
|
||||
fallbackMirror:
|
||||
'https://bgithub.xyz/astral-sh/python-build-standalone/releases/download',
|
||||
validationPathSuffix:
|
||||
'/20250115/cpython-3.10.16+20250115-aarch64-apple-darwin-debug-full.tar.zst.sha256'
|
||||
}
|
||||
|
||||
export const PYPI_MIRROR: UVMirror = {
|
||||
settingId: 'Comfy-Desktop.UV.PypiInstallMirror',
|
||||
mirror: 'https://pypi.org/simple/',
|
||||
fallbackMirror: 'https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple'
|
||||
}
|
||||
@@ -1,17 +1,13 @@
|
||||
import { PYTHON_MIRROR } from '@/constants/uvMirrors'
|
||||
import { t } from '@/i18n'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
|
||||
import { checkMirrorReachable } from '@/utils/networkUtil'
|
||||
|
||||
;(async () => {
|
||||
if (!isElectron()) return
|
||||
|
||||
const electronAPI = getElectronAPI()
|
||||
const desktopAppVersion = await electronAPI.getElectronVersion()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const onChangeRestartApp = (newValue: string, oldValue: string) => {
|
||||
// Add a delay to allow changes to take effect before restarting.
|
||||
@@ -43,52 +39,18 @@ import { checkMirrorReachable } from '@/utils/networkUtil'
|
||||
id: 'Comfy-Desktop.WindowStyle',
|
||||
category: ['Comfy-Desktop', 'General', 'Window Style'],
|
||||
name: 'Window Style',
|
||||
tooltip: "Custom: Replace the system title bar with ComfyUI's Top menu",
|
||||
tooltip: 'Choose custom option to hide the system title bar',
|
||||
type: 'combo',
|
||||
experimental: true,
|
||||
defaultValue: 'default',
|
||||
options: ['default', 'custom'],
|
||||
onChange: (
|
||||
newValue: 'default' | 'custom',
|
||||
oldValue?: 'default' | 'custom'
|
||||
oldValue: 'default' | 'custom'
|
||||
) => {
|
||||
if (!oldValue) return
|
||||
|
||||
electronAPI.Config.setWindowStyle(newValue)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy-Desktop.UV.PythonInstallMirror',
|
||||
name: 'Python Install Mirror',
|
||||
tooltip: `Managed Python installations are downloaded from the Astral python-build-standalone project. This variable can be set to a mirror URL to use a different source for Python installations. The provided URL will replace https://github.com/astral-sh/python-build-standalone/releases/download in, e.g., https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz. Distributions can be read from a local directory by using the file:// URL scheme.`,
|
||||
type: 'url',
|
||||
defaultValue: '',
|
||||
attrs: {
|
||||
validateUrlFn(mirror: string) {
|
||||
return checkMirrorReachable(
|
||||
mirror + PYTHON_MIRROR.validationPathSuffix
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy-Desktop.UV.PypiInstallMirror',
|
||||
name: 'Pypi Install Mirror',
|
||||
tooltip: `Default pip install mirror`,
|
||||
type: 'url',
|
||||
defaultValue: '',
|
||||
attrs: {
|
||||
validateUrlFn: checkMirrorReachable
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy-Desktop.UV.TorchInstallMirror',
|
||||
name: 'Torch Install Mirror',
|
||||
tooltip: `Pip install mirror for pytorch`,
|
||||
type: 'url',
|
||||
defaultValue: '',
|
||||
attrs: {
|
||||
validateUrlFn: checkMirrorReachable
|
||||
|
||||
onChangeRestartApp(newValue, oldValue)
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -150,6 +112,14 @@ import { checkMirrorReachable } from '@/utils/networkUtil'
|
||||
electronAPI.openDevTools()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy-Desktop.OpenFeedbackPage',
|
||||
label: 'Feedback',
|
||||
icon: 'pi pi-envelope',
|
||||
function() {
|
||||
window.open('https://forum.comfy.org/c/v1-feedback/', '_blank')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy-Desktop.OpenUserGuide',
|
||||
label: 'Desktop User Guide',
|
||||
@@ -179,32 +149,16 @@ import { checkMirrorReachable } from '@/utils/networkUtil'
|
||||
function() {
|
||||
electronAPI.restartApp()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy-Desktop.Quit',
|
||||
label: 'Quit',
|
||||
icon: 'pi pi-sign-out',
|
||||
async function() {
|
||||
// Confirm if unsaved workflows are open
|
||||
if (workflowStore.modifiedWorkflows.length > 0) {
|
||||
const confirmed = await useDialogService().confirm({
|
||||
message: t('desktopMenu.confirmQuit'),
|
||||
title: t('desktopMenu.quit'),
|
||||
type: 'default'
|
||||
})
|
||||
|
||||
if (!confirmed) return
|
||||
}
|
||||
|
||||
electronAPI.quit()
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
menuCommands: [
|
||||
{
|
||||
path: ['Help'],
|
||||
commands: ['Comfy-Desktop.OpenUserGuide']
|
||||
commands: [
|
||||
'Comfy-Desktop.OpenUserGuide',
|
||||
'Comfy-Desktop.OpenFeedbackPage'
|
||||
]
|
||||
},
|
||||
{
|
||||
path: ['Help'],
|
||||
@@ -227,16 +181,6 @@ import { checkMirrorReachable } from '@/utils/networkUtil'
|
||||
}
|
||||
],
|
||||
|
||||
keybindings: [
|
||||
{
|
||||
commandId: 'Workspace.CloseWorkflow',
|
||||
combo: {
|
||||
key: 'w',
|
||||
ctrl: true
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
aboutPageBadges: [
|
||||
{
|
||||
label: 'ComfyUI_desktop v' + desktopAppVersion,
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
import type { IWidget } from '@comfyorg/litegraph'
|
||||
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
class Load3DConfiguration {
|
||||
constructor(private load3d: Load3d) {}
|
||||
|
||||
configure(
|
||||
loadFolder: 'input' | 'output',
|
||||
modelWidget: IWidget,
|
||||
material: IWidget,
|
||||
lightIntensity: IWidget,
|
||||
upDirection: IWidget,
|
||||
fov: IWidget,
|
||||
cameraState?: any,
|
||||
postModelUpdateFunc?: (load3d: Load3d) => void
|
||||
) {
|
||||
this.setupModelHandling(
|
||||
modelWidget,
|
||||
loadFolder,
|
||||
cameraState,
|
||||
postModelUpdateFunc
|
||||
)
|
||||
this.setupMaterial(material)
|
||||
this.setupLighting(lightIntensity)
|
||||
this.setupDirection(upDirection)
|
||||
this.setupCamera(fov)
|
||||
this.setupDefaultProperties()
|
||||
}
|
||||
|
||||
private setupModelHandling(
|
||||
modelWidget: IWidget,
|
||||
loadFolder: 'input' | 'output',
|
||||
cameraState?: any,
|
||||
postModelUpdateFunc?: (load3d: Load3d) => void
|
||||
) {
|
||||
const onModelWidgetUpdate = this.createModelUpdateHandler(
|
||||
loadFolder,
|
||||
cameraState,
|
||||
postModelUpdateFunc
|
||||
)
|
||||
if (modelWidget.value) {
|
||||
onModelWidgetUpdate(modelWidget.value)
|
||||
}
|
||||
modelWidget.callback = onModelWidgetUpdate
|
||||
}
|
||||
|
||||
private setupMaterial(material: IWidget) {
|
||||
material.callback = (value: 'original' | 'normal' | 'wireframe') => {
|
||||
this.load3d.setMaterialMode(value)
|
||||
}
|
||||
this.load3d.setMaterialMode(
|
||||
material.value as 'original' | 'normal' | 'wireframe'
|
||||
)
|
||||
}
|
||||
|
||||
private setupLighting(lightIntensity: IWidget) {
|
||||
lightIntensity.callback = (value: number) => {
|
||||
this.load3d.setLightIntensity(value)
|
||||
}
|
||||
this.load3d.setLightIntensity(lightIntensity.value as number)
|
||||
}
|
||||
|
||||
private setupDirection(upDirection: IWidget) {
|
||||
upDirection.callback = (
|
||||
value: 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z'
|
||||
) => {
|
||||
this.load3d.setUpDirection(value)
|
||||
}
|
||||
this.load3d.setUpDirection(
|
||||
upDirection.value as 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z'
|
||||
)
|
||||
}
|
||||
|
||||
private setupCamera(fov: IWidget) {
|
||||
fov.callback = (value: number) => {
|
||||
this.load3d.setFOV(value)
|
||||
}
|
||||
this.load3d.setFOV(fov.value as number)
|
||||
}
|
||||
|
||||
private setupDefaultProperties() {
|
||||
const cameraType = this.load3d.loadNodeProperty(
|
||||
'Camera Type',
|
||||
'perspective'
|
||||
)
|
||||
this.load3d.toggleCamera(cameraType)
|
||||
|
||||
const showGrid = this.load3d.loadNodeProperty('Show Grid', true)
|
||||
this.load3d.toggleGrid(showGrid)
|
||||
|
||||
const bgColor = this.load3d.loadNodeProperty('Background Color', '#282828')
|
||||
|
||||
this.load3d.setBackgroundColor(bgColor)
|
||||
}
|
||||
|
||||
private createModelUpdateHandler(
|
||||
loadFolder: 'input' | 'output',
|
||||
cameraState?: any,
|
||||
postModelUpdateFunc?: (load3d: Load3d) => void
|
||||
) {
|
||||
let isFirstLoad = true
|
||||
return async (value: string | number | boolean | object) => {
|
||||
if (!value) return
|
||||
|
||||
const filename = value as string
|
||||
const modelUrl = api.apiURL(
|
||||
Load3dUtils.getResourceURL(
|
||||
...Load3dUtils.splitFilePath(filename),
|
||||
loadFolder
|
||||
)
|
||||
)
|
||||
|
||||
await this.load3d.loadModel(modelUrl, filename)
|
||||
|
||||
if (postModelUpdateFunc) {
|
||||
postModelUpdateFunc(this.load3d)
|
||||
}
|
||||
|
||||
if (isFirstLoad && cameraState && typeof cameraState === 'object') {
|
||||
try {
|
||||
this.load3d.setCameraState(cameraState)
|
||||
} catch (error) {
|
||||
console.warn('Failed to restore camera state:', error)
|
||||
}
|
||||
isFirstLoad = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Load3DConfiguration
|
||||
@@ -1,997 +0,0 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
|
||||
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
|
||||
import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
|
||||
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
|
||||
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
|
||||
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
|
||||
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
class Load3d {
|
||||
scene: THREE.Scene
|
||||
perspectiveCamera: THREE.PerspectiveCamera
|
||||
orthographicCamera: THREE.OrthographicCamera
|
||||
activeCamera: THREE.Camera
|
||||
renderer: THREE.WebGLRenderer
|
||||
controls: OrbitControls
|
||||
gltfLoader: GLTFLoader
|
||||
objLoader: OBJLoader
|
||||
mtlLoader: MTLLoader
|
||||
fbxLoader: FBXLoader
|
||||
stlLoader: STLLoader
|
||||
currentModel: THREE.Object3D | null = null
|
||||
originalModel: THREE.Object3D | THREE.BufferGeometry | GLTF | null = null
|
||||
animationFrameId: number | null = null
|
||||
gridHelper: THREE.GridHelper
|
||||
lights: THREE.Light[] = []
|
||||
clock: THREE.Clock
|
||||
normalMaterial: THREE.MeshNormalMaterial
|
||||
standardMaterial: THREE.MeshStandardMaterial
|
||||
wireframeMaterial: THREE.MeshBasicMaterial
|
||||
depthMaterial: THREE.MeshDepthMaterial
|
||||
originalMaterials: WeakMap<THREE.Mesh, THREE.Material | THREE.Material[]> =
|
||||
new WeakMap()
|
||||
|
||||
materialMode: 'original' | 'normal' | 'wireframe' | 'depth' = 'original'
|
||||
currentUpDirection: 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z' =
|
||||
'original'
|
||||
originalRotation: THREE.Euler | null = null
|
||||
viewHelper: ViewHelper = {} as ViewHelper
|
||||
viewHelperContainer: HTMLDivElement = {} as HTMLDivElement
|
||||
cameraSwitcherContainer: HTMLDivElement = {} as HTMLDivElement
|
||||
gridSwitcherContainer: HTMLDivElement = {} as HTMLDivElement
|
||||
node: LGraphNode = {} as LGraphNode
|
||||
bgColorInput: HTMLInputElement = {} as HTMLInputElement
|
||||
|
||||
constructor(container: Element | HTMLElement) {
|
||||
this.scene = new THREE.Scene()
|
||||
|
||||
this.perspectiveCamera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000)
|
||||
this.perspectiveCamera.position.set(5, 5, 5)
|
||||
|
||||
const frustumSize = 10
|
||||
this.orthographicCamera = new THREE.OrthographicCamera(
|
||||
-frustumSize / 2,
|
||||
frustumSize / 2,
|
||||
frustumSize / 2,
|
||||
-frustumSize / 2,
|
||||
0.1,
|
||||
1000
|
||||
)
|
||||
this.orthographicCamera.position.set(5, 5, 5)
|
||||
|
||||
this.activeCamera = this.perspectiveCamera
|
||||
|
||||
this.perspectiveCamera.lookAt(0, 0, 0)
|
||||
this.orthographicCamera.lookAt(0, 0, 0)
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true })
|
||||
this.renderer.setSize(300, 300)
|
||||
this.renderer.setClearColor(0x282828)
|
||||
this.renderer.autoClear = false
|
||||
|
||||
const rendererDomElement: HTMLCanvasElement = this.renderer.domElement
|
||||
|
||||
container.appendChild(rendererDomElement)
|
||||
|
||||
this.controls = new OrbitControls(
|
||||
this.activeCamera,
|
||||
this.renderer.domElement
|
||||
)
|
||||
this.controls.enableDamping = true
|
||||
|
||||
this.controls.addEventListener('end', () => {
|
||||
this.storeNodeProperty('Camera Info', this.getCameraState())
|
||||
})
|
||||
|
||||
this.gltfLoader = new GLTFLoader()
|
||||
this.objLoader = new OBJLoader()
|
||||
this.mtlLoader = new MTLLoader()
|
||||
this.fbxLoader = new FBXLoader()
|
||||
this.stlLoader = new STLLoader()
|
||||
this.clock = new THREE.Clock()
|
||||
|
||||
this.setupLights()
|
||||
|
||||
this.gridHelper = new THREE.GridHelper(10, 10)
|
||||
this.gridHelper.position.set(0, 0, 0)
|
||||
this.scene.add(this.gridHelper)
|
||||
|
||||
this.normalMaterial = new THREE.MeshNormalMaterial({
|
||||
flatShading: false,
|
||||
side: THREE.DoubleSide,
|
||||
normalScale: new THREE.Vector2(1, 1),
|
||||
transparent: false,
|
||||
opacity: 1.0
|
||||
})
|
||||
|
||||
this.wireframeMaterial = new THREE.MeshBasicMaterial({
|
||||
color: 0xffffff,
|
||||
wireframe: true,
|
||||
transparent: false,
|
||||
opacity: 1.0
|
||||
})
|
||||
|
||||
this.depthMaterial = new THREE.MeshDepthMaterial({
|
||||
depthPacking: THREE.BasicDepthPacking,
|
||||
side: THREE.DoubleSide
|
||||
})
|
||||
|
||||
this.standardMaterial = this.createSTLMaterial()
|
||||
|
||||
this.createViewHelper(container)
|
||||
|
||||
this.createGridSwitcher(container)
|
||||
|
||||
this.createCameraSwitcher(container)
|
||||
|
||||
this.createColorPicker(container)
|
||||
|
||||
this.handleResize()
|
||||
|
||||
this.startAnimation()
|
||||
}
|
||||
|
||||
setNode(node: LGraphNode) {
|
||||
this.node = node
|
||||
}
|
||||
|
||||
storeNodeProperty(name: string, value: any) {
|
||||
if (this.node) {
|
||||
this.node.properties[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
loadNodeProperty(name: string, defaultValue: any) {
|
||||
if (
|
||||
!this.node ||
|
||||
!this.node.properties ||
|
||||
!(name in this.node.properties)
|
||||
) {
|
||||
return defaultValue
|
||||
}
|
||||
return this.node.properties[name]
|
||||
}
|
||||
|
||||
createViewHelper(container: Element | HTMLElement) {
|
||||
this.viewHelperContainer = document.createElement('div')
|
||||
|
||||
this.viewHelperContainer.style.position = 'absolute'
|
||||
this.viewHelperContainer.style.bottom = '0'
|
||||
this.viewHelperContainer.style.left = '0'
|
||||
this.viewHelperContainer.style.width = '128px'
|
||||
this.viewHelperContainer.style.height = '128px'
|
||||
this.viewHelperContainer.addEventListener('pointerup', (event) => {
|
||||
event.stopPropagation()
|
||||
|
||||
this.viewHelper.handleClick(event)
|
||||
})
|
||||
|
||||
this.viewHelperContainer.addEventListener('pointerdown', (event) => {
|
||||
event.stopPropagation()
|
||||
})
|
||||
|
||||
container.appendChild(this.viewHelperContainer)
|
||||
|
||||
this.viewHelper = new ViewHelper(
|
||||
this.activeCamera,
|
||||
this.viewHelperContainer
|
||||
)
|
||||
|
||||
this.viewHelper.center = this.controls.target
|
||||
}
|
||||
|
||||
createGridSwitcher(container: Element | HTMLElement) {
|
||||
this.gridSwitcherContainer = document.createElement('div')
|
||||
this.gridSwitcherContainer.style.position = 'absolute'
|
||||
this.gridSwitcherContainer.style.top = '28px' // 修改这里,让按钮在相机按钮下方
|
||||
this.gridSwitcherContainer.style.left = '3px' // 与相机按钮左对齐
|
||||
this.gridSwitcherContainer.style.width = '20px'
|
||||
this.gridSwitcherContainer.style.height = '20px'
|
||||
this.gridSwitcherContainer.style.cursor = 'pointer'
|
||||
this.gridSwitcherContainer.style.alignItems = 'center'
|
||||
this.gridSwitcherContainer.style.justifyContent = 'center'
|
||||
this.gridSwitcherContainer.style.transition = 'background-color 0.2s'
|
||||
|
||||
const gridIcon = document.createElement('div')
|
||||
gridIcon.innerHTML = `
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
|
||||
<path d="M3 3h18v18H3z"/>
|
||||
<path d="M3 9h18"/>
|
||||
<path d="M3 15h18"/>
|
||||
<path d="M9 3v18"/>
|
||||
<path d="M15 3v18"/>
|
||||
</svg>
|
||||
`
|
||||
|
||||
const updateButtonState = () => {
|
||||
if (this.gridHelper.visible) {
|
||||
this.gridSwitcherContainer.style.backgroundColor =
|
||||
'rgba(255, 255, 255, 0.2)'
|
||||
} else {
|
||||
this.gridSwitcherContainer.style.backgroundColor = 'transparent'
|
||||
}
|
||||
}
|
||||
|
||||
updateButtonState()
|
||||
|
||||
this.gridSwitcherContainer.addEventListener('mouseenter', () => {
|
||||
if (!this.gridHelper.visible) {
|
||||
this.gridSwitcherContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
})
|
||||
|
||||
this.gridSwitcherContainer.addEventListener('mouseleave', () => {
|
||||
if (!this.gridHelper.visible) {
|
||||
this.gridSwitcherContainer.style.backgroundColor = 'transparent'
|
||||
}
|
||||
})
|
||||
|
||||
this.gridSwitcherContainer.title = 'Toggle Grid'
|
||||
|
||||
this.gridSwitcherContainer.addEventListener('click', (event) => {
|
||||
event.stopPropagation()
|
||||
this.toggleGrid(!this.gridHelper.visible)
|
||||
updateButtonState()
|
||||
})
|
||||
|
||||
this.gridSwitcherContainer.appendChild(gridIcon)
|
||||
container.appendChild(this.gridSwitcherContainer)
|
||||
}
|
||||
|
||||
createCameraSwitcher(container: Element | HTMLElement) {
|
||||
this.cameraSwitcherContainer = document.createElement('div')
|
||||
this.cameraSwitcherContainer.style.position = 'absolute'
|
||||
this.cameraSwitcherContainer.style.top = '3px'
|
||||
this.cameraSwitcherContainer.style.left = '3px'
|
||||
this.cameraSwitcherContainer.style.width = '20px'
|
||||
this.cameraSwitcherContainer.style.height = '20px'
|
||||
this.cameraSwitcherContainer.style.cursor = 'pointer'
|
||||
this.cameraSwitcherContainer.style.alignItems = 'center'
|
||||
this.cameraSwitcherContainer.style.justifyContent = 'center'
|
||||
|
||||
const cameraIcon = document.createElement('div')
|
||||
cameraIcon.innerHTML = `
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
|
||||
<path d="M18 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2Z"/>
|
||||
<path d="m12 12 4-2.4"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</svg>
|
||||
`
|
||||
this.cameraSwitcherContainer.addEventListener('mouseenter', () => {
|
||||
this.cameraSwitcherContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
|
||||
})
|
||||
|
||||
this.cameraSwitcherContainer.addEventListener('mouseleave', () => {
|
||||
this.cameraSwitcherContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.3)'
|
||||
})
|
||||
|
||||
this.cameraSwitcherContainer.title =
|
||||
'Switch Camera (Perspective/Orthographic)'
|
||||
|
||||
this.cameraSwitcherContainer.addEventListener('click', (event) => {
|
||||
event.stopPropagation()
|
||||
this.toggleCamera()
|
||||
})
|
||||
|
||||
this.cameraSwitcherContainer.appendChild(cameraIcon)
|
||||
|
||||
container.appendChild(this.cameraSwitcherContainer)
|
||||
}
|
||||
|
||||
createColorPicker(container: Element | HTMLElement) {
|
||||
const colorPickerContainer = document.createElement('div')
|
||||
colorPickerContainer.style.position = 'absolute'
|
||||
colorPickerContainer.style.top = '53px'
|
||||
colorPickerContainer.style.left = '3px'
|
||||
colorPickerContainer.style.width = '20px'
|
||||
colorPickerContainer.style.height = '20px'
|
||||
colorPickerContainer.style.cursor = 'pointer'
|
||||
colorPickerContainer.style.alignItems = 'center'
|
||||
colorPickerContainer.style.justifyContent = 'center'
|
||||
colorPickerContainer.title = 'Background Color'
|
||||
|
||||
const colorInput = document.createElement('input')
|
||||
colorInput.type = 'color'
|
||||
colorInput.style.opacity = '0'
|
||||
colorInput.style.position = 'absolute'
|
||||
colorInput.style.width = '100%'
|
||||
colorInput.style.height = '100%'
|
||||
colorInput.style.cursor = 'pointer'
|
||||
|
||||
const colorIcon = document.createElement('div')
|
||||
colorIcon.innerHTML = `
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||
<path d="M12 3v18"/>
|
||||
<path d="M3 12h18"/>
|
||||
</svg>
|
||||
`
|
||||
|
||||
colorInput.addEventListener('input', (event) => {
|
||||
const color = (event.target as HTMLInputElement).value
|
||||
this.setBackgroundColor(color)
|
||||
this.storeNodeProperty('Background Color', color)
|
||||
})
|
||||
|
||||
this.bgColorInput = colorInput
|
||||
colorPickerContainer.appendChild(colorInput)
|
||||
colorPickerContainer.appendChild(colorIcon)
|
||||
container.appendChild(colorPickerContainer)
|
||||
}
|
||||
|
||||
setFOV(fov: number) {
|
||||
if (this.activeCamera === this.perspectiveCamera) {
|
||||
this.perspectiveCamera.fov = fov
|
||||
this.perspectiveCamera.updateProjectionMatrix()
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
}
|
||||
}
|
||||
|
||||
getCameraState() {
|
||||
const currentType = this.getCurrentCameraType()
|
||||
return {
|
||||
position: this.activeCamera.position.clone(),
|
||||
target: this.controls.target.clone(),
|
||||
zoom:
|
||||
this.activeCamera instanceof THREE.OrthographicCamera
|
||||
? this.activeCamera.zoom
|
||||
: (this.activeCamera as THREE.PerspectiveCamera).zoom,
|
||||
cameraType: currentType
|
||||
}
|
||||
}
|
||||
|
||||
setCameraState(state: {
|
||||
position: THREE.Vector3
|
||||
target: THREE.Vector3
|
||||
zoom: number
|
||||
cameraType: 'perspective' | 'orthographic'
|
||||
}) {
|
||||
if (
|
||||
this.activeCamera !==
|
||||
(state.cameraType === 'perspective'
|
||||
? this.perspectiveCamera
|
||||
: this.orthographicCamera)
|
||||
) {
|
||||
//this.toggleCamera(state.cameraType)
|
||||
}
|
||||
|
||||
this.activeCamera.position.copy(state.position)
|
||||
|
||||
this.controls.target.copy(state.target)
|
||||
|
||||
if (this.activeCamera instanceof THREE.OrthographicCamera) {
|
||||
this.activeCamera.zoom = state.zoom
|
||||
this.activeCamera.updateProjectionMatrix()
|
||||
} else if (this.activeCamera instanceof THREE.PerspectiveCamera) {
|
||||
this.activeCamera.zoom = state.zoom
|
||||
this.activeCamera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
this.controls.update()
|
||||
}
|
||||
|
||||
setUpDirection(
|
||||
direction: 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z'
|
||||
) {
|
||||
if (!this.currentModel) return
|
||||
|
||||
if (!this.originalRotation && this.currentModel.rotation) {
|
||||
this.originalRotation = this.currentModel.rotation.clone()
|
||||
}
|
||||
|
||||
this.currentUpDirection = direction
|
||||
|
||||
if (this.originalRotation) {
|
||||
this.currentModel.rotation.copy(this.originalRotation)
|
||||
}
|
||||
|
||||
switch (direction) {
|
||||
case 'original':
|
||||
break
|
||||
case '-x':
|
||||
this.currentModel.rotation.z = Math.PI / 2
|
||||
break
|
||||
case '+x':
|
||||
this.currentModel.rotation.z = -Math.PI / 2
|
||||
break
|
||||
case '-y':
|
||||
this.currentModel.rotation.x = Math.PI
|
||||
break
|
||||
case '+y':
|
||||
break
|
||||
case '-z':
|
||||
this.currentModel.rotation.x = Math.PI / 2
|
||||
break
|
||||
case '+z':
|
||||
this.currentModel.rotation.x = -Math.PI / 2
|
||||
break
|
||||
}
|
||||
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
}
|
||||
|
||||
setMaterialMode(mode: 'original' | 'normal' | 'wireframe' | 'depth') {
|
||||
this.materialMode = mode
|
||||
|
||||
if (this.currentModel) {
|
||||
if (mode === 'depth') {
|
||||
this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace
|
||||
} else {
|
||||
this.renderer.outputColorSpace = THREE.SRGBColorSpace
|
||||
}
|
||||
|
||||
this.currentModel.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
switch (mode) {
|
||||
case 'depth':
|
||||
if (!this.originalMaterials.has(child)) {
|
||||
this.originalMaterials.set(child, child.material)
|
||||
}
|
||||
const depthMat = new THREE.MeshDepthMaterial({
|
||||
depthPacking: THREE.BasicDepthPacking,
|
||||
side: THREE.DoubleSide
|
||||
})
|
||||
|
||||
depthMat.onBeforeCompile = (shader) => {
|
||||
shader.uniforms.cameraType = {
|
||||
value:
|
||||
this.activeCamera instanceof THREE.OrthographicCamera
|
||||
? 1.0
|
||||
: 0.0
|
||||
}
|
||||
|
||||
shader.fragmentShader = `
|
||||
uniform float cameraType;
|
||||
${shader.fragmentShader}
|
||||
`
|
||||
|
||||
shader.fragmentShader = shader.fragmentShader.replace(
|
||||
/gl_FragColor\s*=\s*vec4\(\s*vec3\(\s*1.0\s*-\s*fragCoordZ\s*\)\s*,\s*opacity\s*\)\s*;/,
|
||||
`
|
||||
float depth = 1.0 - fragCoordZ;
|
||||
if (cameraType > 0.5) {
|
||||
depth = pow(depth, 400.0);
|
||||
} else {
|
||||
depth = pow(depth, 0.6);
|
||||
}
|
||||
gl_FragColor = vec4(vec3(depth), opacity);
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
depthMat.customProgramCacheKey = () => {
|
||||
return this.activeCamera instanceof THREE.OrthographicCamera
|
||||
? 'ortho'
|
||||
: 'persp'
|
||||
}
|
||||
|
||||
child.material = depthMat
|
||||
break
|
||||
case 'normal':
|
||||
if (!this.originalMaterials.has(child)) {
|
||||
this.originalMaterials.set(child, child.material)
|
||||
}
|
||||
child.material = new THREE.MeshNormalMaterial({
|
||||
flatShading: false,
|
||||
side: THREE.DoubleSide,
|
||||
normalScale: new THREE.Vector2(1, 1),
|
||||
transparent: false,
|
||||
opacity: 1.0
|
||||
})
|
||||
child.geometry.computeVertexNormals()
|
||||
break
|
||||
|
||||
case 'wireframe':
|
||||
if (!this.originalMaterials.has(child)) {
|
||||
this.originalMaterials.set(child, child.material)
|
||||
}
|
||||
child.material = new THREE.MeshBasicMaterial({
|
||||
color: 0xffffff,
|
||||
wireframe: true,
|
||||
transparent: false,
|
||||
opacity: 1.0
|
||||
})
|
||||
break
|
||||
|
||||
case 'original':
|
||||
const originalMaterial = this.originalMaterials.get(child)
|
||||
if (originalMaterial) {
|
||||
child.material = originalMaterial
|
||||
} else {
|
||||
child.material = this.standardMaterial
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
}
|
||||
}
|
||||
|
||||
setupLights() {
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
|
||||
this.scene.add(ambientLight)
|
||||
this.lights.push(ambientLight)
|
||||
|
||||
const mainLight = new THREE.DirectionalLight(0xffffff, 0.8)
|
||||
mainLight.position.set(0, 10, 10)
|
||||
this.scene.add(mainLight)
|
||||
this.lights.push(mainLight)
|
||||
|
||||
const backLight = new THREE.DirectionalLight(0xffffff, 0.5)
|
||||
backLight.position.set(0, 10, -10)
|
||||
this.scene.add(backLight)
|
||||
this.lights.push(backLight)
|
||||
|
||||
const leftFillLight = new THREE.DirectionalLight(0xffffff, 0.3)
|
||||
leftFillLight.position.set(-10, 0, 0)
|
||||
this.scene.add(leftFillLight)
|
||||
this.lights.push(leftFillLight)
|
||||
|
||||
const rightFillLight = new THREE.DirectionalLight(0xffffff, 0.3)
|
||||
rightFillLight.position.set(10, 0, 0)
|
||||
this.scene.add(rightFillLight)
|
||||
this.lights.push(rightFillLight)
|
||||
|
||||
const bottomLight = new THREE.DirectionalLight(0xffffff, 0.2)
|
||||
bottomLight.position.set(0, -10, 0)
|
||||
this.scene.add(bottomLight)
|
||||
this.lights.push(bottomLight)
|
||||
}
|
||||
|
||||
toggleCamera(cameraType?: 'perspective' | 'orthographic') {
|
||||
const oldCamera = this.activeCamera
|
||||
|
||||
const position = oldCamera.position.clone()
|
||||
const rotation = oldCamera.rotation.clone()
|
||||
const target = this.controls.target.clone()
|
||||
|
||||
if (!cameraType) {
|
||||
this.activeCamera =
|
||||
oldCamera === this.perspectiveCamera
|
||||
? this.orthographicCamera
|
||||
: this.perspectiveCamera
|
||||
} else {
|
||||
this.activeCamera =
|
||||
cameraType === 'perspective'
|
||||
? this.perspectiveCamera
|
||||
: this.orthographicCamera
|
||||
|
||||
if (oldCamera === this.activeCamera) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.activeCamera.position.copy(position)
|
||||
this.activeCamera.rotation.copy(rotation)
|
||||
|
||||
if (this.materialMode === 'depth' && oldCamera !== this.activeCamera) {
|
||||
this.setMaterialMode('depth')
|
||||
}
|
||||
|
||||
this.controls.object = this.activeCamera
|
||||
this.controls.target.copy(target)
|
||||
this.controls.update()
|
||||
|
||||
this.viewHelper.dispose()
|
||||
this.viewHelper = new ViewHelper(
|
||||
this.activeCamera,
|
||||
this.viewHelperContainer
|
||||
)
|
||||
this.viewHelper.center = this.controls.target
|
||||
|
||||
this.storeNodeProperty('Camera Type', this.getCurrentCameraType())
|
||||
this.handleResize()
|
||||
}
|
||||
|
||||
getCurrentCameraType(): 'perspective' | 'orthographic' {
|
||||
return this.activeCamera === this.perspectiveCamera
|
||||
? 'perspective'
|
||||
: 'orthographic'
|
||||
}
|
||||
|
||||
toggleGrid(showGrid: boolean) {
|
||||
if (this.gridHelper) {
|
||||
this.gridHelper.visible = showGrid
|
||||
|
||||
this.storeNodeProperty('Show Grid', showGrid)
|
||||
}
|
||||
}
|
||||
|
||||
setLightIntensity(intensity: number) {
|
||||
this.lights.forEach((light) => {
|
||||
if (light instanceof THREE.DirectionalLight) {
|
||||
if (light === this.lights[1]) {
|
||||
light.intensity = intensity * 0.8
|
||||
} else if (light === this.lights[2]) {
|
||||
light.intensity = intensity * 0.5
|
||||
} else if (light === this.lights[5]) {
|
||||
light.intensity = intensity * 0.2
|
||||
} else {
|
||||
light.intensity = intensity * 0.3
|
||||
}
|
||||
} else if (light instanceof THREE.AmbientLight) {
|
||||
light.intensity = intensity * 0.5
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
startAnimation() {
|
||||
const animate = () => {
|
||||
this.animationFrameId = requestAnimationFrame(animate)
|
||||
const delta = this.clock.getDelta()
|
||||
|
||||
if (this.viewHelper.animating) {
|
||||
this.viewHelper.update(delta)
|
||||
|
||||
if (!this.viewHelper.animating) {
|
||||
this.storeNodeProperty('Camera Info', this.getCameraState())
|
||||
}
|
||||
}
|
||||
|
||||
this.renderer.clear()
|
||||
this.controls.update()
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
this.viewHelper.render(this.renderer)
|
||||
}
|
||||
animate()
|
||||
}
|
||||
|
||||
clearModel() {
|
||||
const objectsToRemove: THREE.Object3D[] = []
|
||||
|
||||
this.scene.traverse((object) => {
|
||||
const isEnvironmentObject =
|
||||
object === this.gridHelper ||
|
||||
this.lights.includes(object as THREE.Light) ||
|
||||
object === this.perspectiveCamera ||
|
||||
object === this.orthographicCamera
|
||||
|
||||
if (!isEnvironmentObject) {
|
||||
objectsToRemove.push(object)
|
||||
}
|
||||
})
|
||||
|
||||
objectsToRemove.forEach((obj) => {
|
||||
if (obj.parent && obj.parent !== this.scene) {
|
||||
obj.parent.remove(obj)
|
||||
} else {
|
||||
this.scene.remove(obj)
|
||||
}
|
||||
|
||||
if (obj instanceof THREE.Mesh) {
|
||||
obj.geometry?.dispose()
|
||||
if (Array.isArray(obj.material)) {
|
||||
obj.material.forEach((material) => material.dispose())
|
||||
} else {
|
||||
obj.material?.dispose()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.resetScene()
|
||||
}
|
||||
|
||||
protected resetScene() {
|
||||
this.currentModel = null
|
||||
this.originalRotation = null
|
||||
|
||||
const defaultDistance = 10
|
||||
this.perspectiveCamera.position.set(
|
||||
defaultDistance,
|
||||
defaultDistance,
|
||||
defaultDistance
|
||||
)
|
||||
this.orthographicCamera.position.set(
|
||||
defaultDistance,
|
||||
defaultDistance,
|
||||
defaultDistance
|
||||
)
|
||||
|
||||
this.perspectiveCamera.lookAt(0, 0, 0)
|
||||
this.orthographicCamera.lookAt(0, 0, 0)
|
||||
|
||||
const frustumSize = 10
|
||||
const aspect =
|
||||
this.renderer.domElement.width / this.renderer.domElement.height
|
||||
this.orthographicCamera.left = (-frustumSize * aspect) / 2
|
||||
this.orthographicCamera.right = (frustumSize * aspect) / 2
|
||||
this.orthographicCamera.top = frustumSize / 2
|
||||
this.orthographicCamera.bottom = -frustumSize / 2
|
||||
|
||||
this.perspectiveCamera.updateProjectionMatrix()
|
||||
this.orthographicCamera.updateProjectionMatrix()
|
||||
|
||||
this.controls.target.set(0, 0, 0)
|
||||
this.controls.update()
|
||||
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
|
||||
this.materialMode = 'original'
|
||||
this.originalMaterials = new WeakMap()
|
||||
this.renderer.outputColorSpace = THREE.SRGBColorSpace
|
||||
}
|
||||
|
||||
remove() {
|
||||
if (this.animationFrameId !== null) {
|
||||
cancelAnimationFrame(this.animationFrameId)
|
||||
}
|
||||
|
||||
this.controls.dispose()
|
||||
this.viewHelper.dispose()
|
||||
this.renderer.dispose()
|
||||
this.renderer.domElement.remove()
|
||||
this.scene.clear()
|
||||
}
|
||||
|
||||
protected async loadModelInternal(
|
||||
url: string,
|
||||
fileExtension: string
|
||||
): Promise<THREE.Object3D | null> {
|
||||
let model: THREE.Object3D | null = null
|
||||
|
||||
switch (fileExtension) {
|
||||
case 'stl':
|
||||
const geometry = await this.stlLoader.loadAsync(url)
|
||||
|
||||
this.originalModel = geometry
|
||||
|
||||
geometry.computeVertexNormals()
|
||||
const mesh = new THREE.Mesh(geometry, this.standardMaterial)
|
||||
const group = new THREE.Group()
|
||||
group.add(mesh)
|
||||
model = group
|
||||
break
|
||||
|
||||
case 'fbx':
|
||||
const fbxModel = await this.fbxLoader.loadAsync(url)
|
||||
|
||||
this.originalModel = fbxModel
|
||||
|
||||
model = fbxModel
|
||||
|
||||
fbxModel.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
this.originalMaterials.set(child, child.material)
|
||||
}
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
case 'obj':
|
||||
if (this.materialMode === 'original') {
|
||||
const mtlUrl = url.replace(/\.obj([^.]*$)/, '.mtl$1')
|
||||
try {
|
||||
const materials = await this.mtlLoader.loadAsync(mtlUrl)
|
||||
materials.preload()
|
||||
this.objLoader.setMaterials(materials)
|
||||
} catch (e) {
|
||||
console.log(
|
||||
'No MTL file found or error loading it, continuing without materials'
|
||||
)
|
||||
}
|
||||
}
|
||||
model = await this.objLoader.loadAsync(url)
|
||||
model.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
this.originalMaterials.set(child, child.material)
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
case 'gltf':
|
||||
case 'glb':
|
||||
const gltf = await this.gltfLoader.loadAsync(url)
|
||||
|
||||
this.originalModel = gltf
|
||||
|
||||
model = gltf.scene
|
||||
gltf.scene.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.geometry.computeVertexNormals()
|
||||
this.originalMaterials.set(child, child.material)
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
return model
|
||||
}
|
||||
|
||||
async loadModel(url: string, originalFileName?: string) {
|
||||
try {
|
||||
this.clearModel()
|
||||
|
||||
let fileExtension: string | undefined
|
||||
if (originalFileName) {
|
||||
fileExtension = originalFileName.split('.').pop()?.toLowerCase()
|
||||
} else {
|
||||
const filename = new URLSearchParams(url.split('?')[1]).get('filename')
|
||||
fileExtension = filename?.split('.').pop()?.toLowerCase()
|
||||
}
|
||||
|
||||
if (!fileExtension) {
|
||||
useToastStore().addAlert('Could not determine file type')
|
||||
return
|
||||
}
|
||||
|
||||
let model = await this.loadModelInternal(url, fileExtension)
|
||||
|
||||
if (model) {
|
||||
this.currentModel = model
|
||||
await this.setupModel(model)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading model:', error)
|
||||
}
|
||||
}
|
||||
|
||||
protected async setupModel(model: THREE.Object3D) {
|
||||
const box = new THREE.Box3().setFromObject(model)
|
||||
const size = box.getSize(new THREE.Vector3())
|
||||
const center = box.getCenter(new THREE.Vector3())
|
||||
|
||||
const maxDim = Math.max(size.x, size.y, size.z)
|
||||
const targetSize = 5
|
||||
const scale = targetSize / maxDim
|
||||
model.scale.multiplyScalar(scale)
|
||||
|
||||
box.setFromObject(model)
|
||||
box.getCenter(center)
|
||||
box.getSize(size)
|
||||
|
||||
model.position.set(-center.x, -box.min.y, -center.z)
|
||||
|
||||
this.scene.add(model)
|
||||
|
||||
if (this.materialMode !== 'original') {
|
||||
this.setMaterialMode(this.materialMode)
|
||||
}
|
||||
|
||||
if (this.currentUpDirection !== 'original') {
|
||||
this.setUpDirection(this.currentUpDirection)
|
||||
}
|
||||
|
||||
await this.setupCamera(size)
|
||||
}
|
||||
|
||||
protected async setupCamera(size: THREE.Vector3) {
|
||||
const distance = Math.max(size.x, size.z) * 2
|
||||
const height = size.y * 2
|
||||
|
||||
this.perspectiveCamera.position.set(distance, height, distance)
|
||||
this.orthographicCamera.position.set(distance, height, distance)
|
||||
|
||||
if (this.activeCamera === this.perspectiveCamera) {
|
||||
this.perspectiveCamera.lookAt(0, size.y / 2, 0)
|
||||
this.perspectiveCamera.updateProjectionMatrix()
|
||||
} else {
|
||||
const frustumSize = Math.max(size.x, size.y, size.z) * 2
|
||||
const aspect =
|
||||
this.renderer.domElement.width / this.renderer.domElement.height
|
||||
this.orthographicCamera.left = (-frustumSize * aspect) / 2
|
||||
this.orthographicCamera.right = (frustumSize * aspect) / 2
|
||||
this.orthographicCamera.top = frustumSize / 2
|
||||
this.orthographicCamera.bottom = -frustumSize / 2
|
||||
this.orthographicCamera.lookAt(0, size.y / 2, 0)
|
||||
this.orthographicCamera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
this.controls.target.set(0, size.y / 2, 0)
|
||||
this.controls.update()
|
||||
|
||||
this.renderer.outputColorSpace = THREE.SRGBColorSpace
|
||||
this.renderer.toneMapping = THREE.ACESFilmicToneMapping
|
||||
this.renderer.toneMappingExposure = 1
|
||||
|
||||
this.handleResize()
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
const parentElement = this.renderer?.domElement?.parentElement
|
||||
|
||||
if (!parentElement) {
|
||||
console.warn('Parent element not found')
|
||||
return
|
||||
}
|
||||
|
||||
const width = parentElement?.clientWidth
|
||||
const height = parentElement?.clientHeight
|
||||
|
||||
if (this.activeCamera === this.perspectiveCamera) {
|
||||
this.perspectiveCamera.aspect = width / height
|
||||
this.perspectiveCamera.updateProjectionMatrix()
|
||||
} else {
|
||||
const frustumSize = 10
|
||||
const aspect = width / height
|
||||
this.orthographicCamera.left = (-frustumSize * aspect) / 2
|
||||
this.orthographicCamera.right = (frustumSize * aspect) / 2
|
||||
this.orthographicCamera.top = frustumSize / 2
|
||||
this.orthographicCamera.bottom = -frustumSize / 2
|
||||
this.orthographicCamera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
this.renderer.setSize(width, height)
|
||||
}
|
||||
|
||||
animate = () => {
|
||||
requestAnimationFrame(this.animate)
|
||||
|
||||
this.controls.update()
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
}
|
||||
|
||||
captureScene(
|
||||
width: number,
|
||||
height: number
|
||||
): Promise<{ scene: string; mask: string }> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const originalWidth = this.renderer.domElement.width
|
||||
const originalHeight = this.renderer.domElement.height
|
||||
const originalClearColor = this.renderer.getClearColor(
|
||||
new THREE.Color()
|
||||
)
|
||||
const originalClearAlpha = this.renderer.getClearAlpha()
|
||||
|
||||
this.renderer.setSize(width, height)
|
||||
|
||||
if (this.activeCamera === this.perspectiveCamera) {
|
||||
this.perspectiveCamera.aspect = width / height
|
||||
this.perspectiveCamera.updateProjectionMatrix()
|
||||
} else {
|
||||
const frustumSize = 10
|
||||
const aspect = width / height
|
||||
this.orthographicCamera.left = (-frustumSize * aspect) / 2
|
||||
this.orthographicCamera.right = (frustumSize * aspect) / 2
|
||||
this.orthographicCamera.top = frustumSize / 2
|
||||
this.orthographicCamera.bottom = -frustumSize / 2
|
||||
this.orthographicCamera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
this.renderer.clear()
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
const sceneData = this.renderer.domElement.toDataURL('image/png')
|
||||
|
||||
this.renderer.setClearColor(0x000000, 0)
|
||||
this.renderer.clear()
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
const maskData = this.renderer.domElement.toDataURL('image/png')
|
||||
|
||||
this.renderer.setClearColor(originalClearColor, originalClearAlpha)
|
||||
this.renderer.setSize(originalWidth, originalHeight)
|
||||
this.handleResize()
|
||||
|
||||
resolve({ scene: sceneData, mask: maskData })
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
createSTLMaterial() {
|
||||
return new THREE.MeshStandardMaterial({
|
||||
color: 0x808080,
|
||||
metalness: 0.1,
|
||||
roughness: 0.8,
|
||||
flatShading: false,
|
||||
side: THREE.DoubleSide
|
||||
})
|
||||
}
|
||||
|
||||
setBackgroundColor(color: string) {
|
||||
this.renderer.setClearColor(new THREE.Color(color))
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
|
||||
if (this.bgColorInput) {
|
||||
this.bgColorInput.value = color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Load3d
|
||||
@@ -1,359 +0,0 @@
|
||||
import * as THREE from 'three'
|
||||
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
|
||||
class Load3dAnimation extends Load3d {
|
||||
currentAnimation: THREE.AnimationMixer | null = null
|
||||
animationActions: THREE.AnimationAction[] = []
|
||||
animationClips: THREE.AnimationClip[] = []
|
||||
selectedAnimationIndex: number = 0
|
||||
isAnimationPlaying: boolean = false
|
||||
|
||||
animationSpeed: number = 1.0
|
||||
playPauseContainer: HTMLDivElement = {} as HTMLDivElement
|
||||
animationSelect: HTMLSelectElement = {} as HTMLSelectElement
|
||||
speedSelect: HTMLSelectElement = {} as HTMLSelectElement
|
||||
|
||||
constructor(container: Element | HTMLElement) {
|
||||
super(container)
|
||||
this.createPlayPauseButton(container)
|
||||
this.createAnimationList(container)
|
||||
this.createSpeedSelect(container)
|
||||
}
|
||||
|
||||
createAnimationList(container: Element | HTMLElement) {
|
||||
this.animationSelect = document.createElement('select')
|
||||
Object.assign(this.animationSelect.style, {
|
||||
position: 'absolute',
|
||||
top: '3px',
|
||||
left: '50%',
|
||||
transform: 'translateX(15px)',
|
||||
width: '90px',
|
||||
height: '20px',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
padding: '0 8px',
|
||||
cursor: 'pointer',
|
||||
display: 'none',
|
||||
outline: 'none'
|
||||
})
|
||||
|
||||
this.animationSelect.addEventListener('mouseenter', () => {
|
||||
this.animationSelect.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
|
||||
})
|
||||
|
||||
this.animationSelect.addEventListener('mouseleave', () => {
|
||||
this.animationSelect.style.backgroundColor = 'rgba(0, 0, 0, 0.3)'
|
||||
})
|
||||
|
||||
this.animationSelect.addEventListener('change', (event) => {
|
||||
const select = event.target as HTMLSelectElement
|
||||
this.updateSelectedAnimation(select.selectedIndex)
|
||||
})
|
||||
|
||||
container.appendChild(this.animationSelect)
|
||||
}
|
||||
|
||||
updateAnimationList() {
|
||||
this.animationSelect.innerHTML = ''
|
||||
this.animationClips.forEach((clip, index) => {
|
||||
const option = document.createElement('option')
|
||||
option.value = index.toString()
|
||||
option.text = clip.name || `Animation ${index + 1}`
|
||||
option.selected = index === this.selectedAnimationIndex
|
||||
this.animationSelect.appendChild(option)
|
||||
})
|
||||
}
|
||||
|
||||
createPlayPauseButton(container: Element | HTMLElement) {
|
||||
this.playPauseContainer = document.createElement('div')
|
||||
this.playPauseContainer.style.position = 'absolute'
|
||||
this.playPauseContainer.style.top = '3px'
|
||||
this.playPauseContainer.style.left = '50%'
|
||||
this.playPauseContainer.style.transform = 'translateX(-50%)'
|
||||
this.playPauseContainer.style.width = '20px'
|
||||
this.playPauseContainer.style.height = '20px'
|
||||
this.playPauseContainer.style.cursor = 'pointer'
|
||||
this.playPauseContainer.style.alignItems = 'center'
|
||||
this.playPauseContainer.style.justifyContent = 'center'
|
||||
|
||||
const updateButtonState = () => {
|
||||
const icon = this.playPauseContainer.querySelector('svg')
|
||||
if (icon) {
|
||||
if (this.isAnimationPlaying) {
|
||||
icon.innerHTML = `
|
||||
<path d="M6 4h4v16H6zM14 4h4v16h-4z"/>
|
||||
`
|
||||
this.playPauseContainer.title = 'Pause Animation'
|
||||
} else {
|
||||
icon.innerHTML = `
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
`
|
||||
this.playPauseContainer.title = 'Play Animation'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const playIcon = document.createElement('div')
|
||||
playIcon.innerHTML = `
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
</svg>
|
||||
`
|
||||
|
||||
this.playPauseContainer.addEventListener('mouseenter', () => {
|
||||
this.playPauseContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
|
||||
})
|
||||
|
||||
this.playPauseContainer.addEventListener('mouseleave', () => {
|
||||
this.playPauseContainer.style.backgroundColor = 'transparent'
|
||||
})
|
||||
|
||||
this.playPauseContainer.addEventListener('click', (event) => {
|
||||
event.stopPropagation()
|
||||
this.toggleAnimation()
|
||||
updateButtonState()
|
||||
})
|
||||
|
||||
this.playPauseContainer.appendChild(playIcon)
|
||||
container.appendChild(this.playPauseContainer)
|
||||
|
||||
this.playPauseContainer.style.display = 'none'
|
||||
}
|
||||
|
||||
createSpeedSelect(container: Element | HTMLElement) {
|
||||
this.speedSelect = document.createElement('select')
|
||||
Object.assign(this.speedSelect.style, {
|
||||
position: 'absolute',
|
||||
top: '3px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-75px)',
|
||||
width: '60px',
|
||||
height: '20px',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
padding: '0 8px',
|
||||
cursor: 'pointer',
|
||||
display: 'none',
|
||||
outline: 'none'
|
||||
})
|
||||
|
||||
const speeds = [0.1, 0.5, 1, 1.5, 2]
|
||||
speeds.forEach((speed) => {
|
||||
const option = document.createElement('option')
|
||||
option.value = speed.toString()
|
||||
option.text = `${speed}x`
|
||||
option.selected = speed === 1
|
||||
this.speedSelect.appendChild(option)
|
||||
})
|
||||
|
||||
this.speedSelect.addEventListener('mouseenter', () => {
|
||||
this.speedSelect.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
|
||||
})
|
||||
|
||||
this.speedSelect.addEventListener('mouseleave', () => {
|
||||
this.speedSelect.style.backgroundColor = 'rgba(0, 0, 0, 0.3)'
|
||||
})
|
||||
|
||||
this.speedSelect.addEventListener('change', (event) => {
|
||||
const select = event.target as HTMLSelectElement
|
||||
const newSpeed = parseFloat(select.value)
|
||||
this.setAnimationSpeed(newSpeed)
|
||||
})
|
||||
|
||||
container.appendChild(this.speedSelect)
|
||||
}
|
||||
|
||||
protected async setupModel(model: THREE.Object3D) {
|
||||
await super.setupModel(model)
|
||||
|
||||
if (this.currentAnimation) {
|
||||
this.currentAnimation.stopAllAction()
|
||||
this.animationActions = []
|
||||
}
|
||||
|
||||
let animations: THREE.AnimationClip[] = []
|
||||
if (model.animations?.length > 0) {
|
||||
animations = model.animations
|
||||
} else if (this.originalModel && 'animations' in this.originalModel) {
|
||||
animations = (
|
||||
this.originalModel as unknown as { animations: THREE.AnimationClip[] }
|
||||
).animations
|
||||
}
|
||||
|
||||
if (animations.length > 0) {
|
||||
this.animationClips = animations
|
||||
if (model.type === 'Scene') {
|
||||
this.currentAnimation = new THREE.AnimationMixer(model)
|
||||
} else {
|
||||
this.currentAnimation = new THREE.AnimationMixer(this.currentModel!)
|
||||
}
|
||||
|
||||
if (this.animationClips.length > 0) {
|
||||
this.updateSelectedAnimation(0)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.animationClips.length > 0) {
|
||||
this.playPauseContainer.style.display = 'block'
|
||||
} else {
|
||||
this.playPauseContainer.style.display = 'none'
|
||||
}
|
||||
|
||||
if (this.animationClips.length > 0) {
|
||||
this.playPauseContainer.style.display = 'block'
|
||||
this.animationSelect.style.display = 'block'
|
||||
this.speedSelect.style.display = 'block'
|
||||
this.updateAnimationList()
|
||||
} else {
|
||||
this.playPauseContainer.style.display = 'none'
|
||||
this.animationSelect.style.display = 'none'
|
||||
this.speedSelect.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
setAnimationSpeed(speed: number) {
|
||||
this.animationSpeed = speed
|
||||
this.animationActions.forEach((action) => {
|
||||
action.setEffectiveTimeScale(speed)
|
||||
})
|
||||
}
|
||||
|
||||
updateSelectedAnimation(index: number) {
|
||||
if (
|
||||
!this.currentAnimation ||
|
||||
!this.animationClips ||
|
||||
index >= this.animationClips.length
|
||||
) {
|
||||
console.warn('Invalid animation update request')
|
||||
return
|
||||
}
|
||||
|
||||
this.animationActions.forEach((action) => {
|
||||
action.stop()
|
||||
})
|
||||
this.currentAnimation.stopAllAction()
|
||||
this.animationActions = []
|
||||
|
||||
this.selectedAnimationIndex = index
|
||||
const clip = this.animationClips[index]
|
||||
|
||||
const action = this.currentAnimation.clipAction(clip)
|
||||
|
||||
action.setEffectiveTimeScale(this.animationSpeed)
|
||||
|
||||
action.reset()
|
||||
action.clampWhenFinished = false
|
||||
action.loop = THREE.LoopRepeat
|
||||
|
||||
if (this.isAnimationPlaying) {
|
||||
action.play()
|
||||
} else {
|
||||
action.play()
|
||||
action.paused = true
|
||||
}
|
||||
|
||||
this.animationActions = [action]
|
||||
|
||||
this.updateAnimationList()
|
||||
}
|
||||
|
||||
clearModel() {
|
||||
if (this.currentAnimation) {
|
||||
this.animationActions.forEach((action) => {
|
||||
action.stop()
|
||||
})
|
||||
this.currentAnimation = null
|
||||
}
|
||||
this.animationActions = []
|
||||
this.animationClips = []
|
||||
this.selectedAnimationIndex = 0
|
||||
this.isAnimationPlaying = false
|
||||
this.animationSpeed = 1.0
|
||||
|
||||
super.clearModel()
|
||||
|
||||
if (this.animationSelect) {
|
||||
this.animationSelect.style.display = 'none'
|
||||
this.animationSelect.innerHTML = ''
|
||||
}
|
||||
|
||||
if (this.speedSelect) {
|
||||
this.speedSelect.style.display = 'none'
|
||||
this.speedSelect.value = '1'
|
||||
}
|
||||
}
|
||||
|
||||
getAnimationNames(): string[] {
|
||||
return this.animationClips.map((clip, index) => {
|
||||
return clip.name || `Animation ${index + 1}`
|
||||
})
|
||||
}
|
||||
|
||||
toggleAnimation(play?: boolean) {
|
||||
if (!this.currentAnimation || this.animationActions.length === 0) {
|
||||
console.warn('No animation to toggle')
|
||||
return
|
||||
}
|
||||
|
||||
this.isAnimationPlaying = play ?? !this.isAnimationPlaying
|
||||
|
||||
const icon = this.playPauseContainer.querySelector('svg')
|
||||
if (icon) {
|
||||
if (this.isAnimationPlaying) {
|
||||
icon.innerHTML = '<path d="M6 4h4v16H6zM14 4h4v16h-4z"/>'
|
||||
this.playPauseContainer.title = 'Pause Animation'
|
||||
} else {
|
||||
icon.innerHTML = '<path d="M8 5v14l11-7z"/>'
|
||||
this.playPauseContainer.title = 'Play Animation'
|
||||
}
|
||||
}
|
||||
|
||||
this.animationActions.forEach((action) => {
|
||||
if (this.isAnimationPlaying) {
|
||||
action.paused = false
|
||||
if (action.time === 0 || action.time === action.getClip().duration) {
|
||||
action.reset()
|
||||
}
|
||||
} else {
|
||||
action.paused = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
startAnimation() {
|
||||
const animate = () => {
|
||||
this.animationFrameId = requestAnimationFrame(animate)
|
||||
const delta = this.clock.getDelta()
|
||||
|
||||
if (this.currentAnimation && this.isAnimationPlaying) {
|
||||
this.currentAnimation.update(delta)
|
||||
}
|
||||
|
||||
this.controls.update()
|
||||
|
||||
this.renderer.clear()
|
||||
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
|
||||
if (this.viewHelper.animating) {
|
||||
this.viewHelper.update(delta)
|
||||
|
||||
if (!this.viewHelper.animating) {
|
||||
this.storeNodeProperty('Camera Info', this.getCameraState())
|
||||
}
|
||||
}
|
||||
|
||||
this.viewHelper.render(this.renderer)
|
||||
}
|
||||
animate()
|
||||
}
|
||||
}
|
||||
|
||||
export default Load3dAnimation
|
||||
@@ -1,122 +0,0 @@
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
class Load3dUtils {
|
||||
static async uploadTempImage(imageData: string, prefix: string) {
|
||||
const blob = await fetch(imageData).then((r) => r.blob())
|
||||
const name = `${prefix}_${Date.now()}.png`
|
||||
const file = new File([blob], name)
|
||||
|
||||
const body = new FormData()
|
||||
body.append('image', file)
|
||||
body.append('subfolder', 'threed')
|
||||
body.append('type', 'temp')
|
||||
|
||||
const resp = await api.fetchApi('/upload/image', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
|
||||
if (resp.status !== 200) {
|
||||
const err = `Error uploading temp image: ${resp.status} - ${resp.statusText}`
|
||||
useToastStore().addAlert(err)
|
||||
throw new Error(err)
|
||||
}
|
||||
|
||||
return await resp.json()
|
||||
}
|
||||
|
||||
static async uploadFile(
|
||||
load3d: Load3d,
|
||||
file: File,
|
||||
fileInput?: HTMLInputElement
|
||||
) {
|
||||
let uploadPath
|
||||
|
||||
try {
|
||||
const body = new FormData()
|
||||
body.append('image', file)
|
||||
body.append('subfolder', '3d')
|
||||
|
||||
const resp = await api.fetchApi('/upload/image', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
|
||||
if (resp.status === 200) {
|
||||
const data = await resp.json()
|
||||
let path = data.name
|
||||
|
||||
if (data.subfolder) path = data.subfolder + '/' + path
|
||||
|
||||
uploadPath = path
|
||||
|
||||
const modelUrl = api.apiURL(
|
||||
this.getResourceURL(...this.splitFilePath(path), 'input')
|
||||
)
|
||||
await load3d.loadModel(modelUrl, file.name)
|
||||
|
||||
const fileExt = file.name.split('.').pop()?.toLowerCase()
|
||||
if (fileExt === 'obj' && fileInput?.files) {
|
||||
try {
|
||||
const mtlFile = Array.from(fileInput.files).find((f) =>
|
||||
f.name.toLowerCase().endsWith('.mtl')
|
||||
)
|
||||
|
||||
if (mtlFile) {
|
||||
const mtlFormData = new FormData()
|
||||
mtlFormData.append('image', mtlFile)
|
||||
mtlFormData.append('subfolder', '3d')
|
||||
|
||||
await api.fetchApi('/upload/image', {
|
||||
method: 'POST',
|
||||
body: mtlFormData
|
||||
})
|
||||
}
|
||||
} catch (mtlError) {
|
||||
console.warn('Failed to upload MTL file:', mtlError)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
useToastStore().addAlert(resp.status + ' - ' + resp.statusText)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error)
|
||||
useToastStore().addAlert(
|
||||
error instanceof Error ? error.message : 'Upload failed'
|
||||
)
|
||||
}
|
||||
|
||||
return uploadPath
|
||||
}
|
||||
|
||||
static splitFilePath(path: string): [string, string] {
|
||||
const folder_separator = path.lastIndexOf('/')
|
||||
if (folder_separator === -1) {
|
||||
return ['', path]
|
||||
}
|
||||
return [
|
||||
path.substring(0, folder_separator),
|
||||
path.substring(folder_separator + 1)
|
||||
]
|
||||
}
|
||||
|
||||
static getResourceURL(
|
||||
subfolder: string,
|
||||
filename: string,
|
||||
type: string = 'input'
|
||||
): string {
|
||||
const params = [
|
||||
'filename=' + encodeURIComponent(filename),
|
||||
'type=' + type,
|
||||
'subfolder=' + subfolder,
|
||||
app.getRandParam().substring(1)
|
||||
].join('&')
|
||||
|
||||
return `/view?${params}`
|
||||
}
|
||||
}
|
||||
|
||||
export default Load3dUtils
|
||||
@@ -1,10 +1,7 @@
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
import { api } from '../../scripts/api'
|
||||
import { app } from '../../scripts/app'
|
||||
import { ComfyApp } from '../../scripts/app'
|
||||
import { $el, ComfyDialog } from '../../scripts/ui'
|
||||
import { getStorageValue, setStorageValue } from '../../scripts/utils'
|
||||
import { ClipspaceDialog } from './clipspace'
|
||||
import { MaskEditorDialogOld } from './maskEditorOld'
|
||||
|
||||
@@ -265,15 +262,15 @@ var styles = `
|
||||
}
|
||||
#maskEditor_toolPanel {
|
||||
height: 100%;
|
||||
width: 4rem;
|
||||
width: var(--sidebar-width);
|
||||
z-index: 8888;
|
||||
background: var(--comfy-menu-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.maskEditor_toolPanelContainer {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
width: var(--sidebar-width);
|
||||
height: var(--sidebar-width);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -334,7 +331,7 @@ var styles = `
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
#maskEditor_pointerZone {
|
||||
width: calc(100% - 4rem - 220px);
|
||||
width: calc(100% - var(--sidebar-width) - 220px);
|
||||
height: 100%;
|
||||
}
|
||||
#maskEditor_uiContainer {
|
||||
@@ -706,8 +703,8 @@ var styles = `
|
||||
}
|
||||
|
||||
.maskEditor_toolPanelZoomIndicator {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
width: var(--sidebar-width);
|
||||
height: var(--sidebar-width);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
@@ -779,37 +776,10 @@ interface Offset {
|
||||
}
|
||||
|
||||
export interface Brush {
|
||||
type: BrushShape
|
||||
size: number
|
||||
opacity: number
|
||||
hardness: number
|
||||
smoothingPrecision: number
|
||||
}
|
||||
|
||||
const saveBrushToCache = debounce(function (key: string, brush: Brush): void {
|
||||
try {
|
||||
const brushString = JSON.stringify(brush)
|
||||
setStorageValue(key, brushString)
|
||||
} catch (error) {
|
||||
console.error('Failed to save brush to cache:', error)
|
||||
}
|
||||
}, 300)
|
||||
|
||||
function loadBrushFromCache(key: string): Brush | null {
|
||||
try {
|
||||
const brushString = getStorageValue(key)
|
||||
if (brushString) {
|
||||
const brush = JSON.parse(brushString) as Brush
|
||||
console.log('Loaded brush from cache:', brush)
|
||||
return brush
|
||||
} else {
|
||||
console.log('No brush found in cache.')
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load brush from cache:', error)
|
||||
return null
|
||||
}
|
||||
type: BrushShape
|
||||
}
|
||||
|
||||
type Callback = (data?: any) => void
|
||||
@@ -1982,19 +1952,12 @@ class BrushTool {
|
||||
'Comfy.MaskEditor.BrushAdjustmentSpeed'
|
||||
)
|
||||
|
||||
const cachedBrushSettings = loadBrushFromCache('maskeditor_brush_settings')
|
||||
if (cachedBrushSettings) {
|
||||
this.brushSettings = cachedBrushSettings
|
||||
} else {
|
||||
this.brushSettings = {
|
||||
type: BrushShape.Arc,
|
||||
size: 10,
|
||||
opacity: 0.7,
|
||||
hardness: 1,
|
||||
smoothingPrecision: 10
|
||||
}
|
||||
this.brushSettings = {
|
||||
size: 10,
|
||||
opacity: 100,
|
||||
hardness: 1,
|
||||
type: BrushShape.Arc
|
||||
}
|
||||
|
||||
this.maskBlendMode = MaskBlendMode.Black
|
||||
}
|
||||
|
||||
@@ -2053,10 +2016,6 @@ class BrushTool {
|
||||
'brushType',
|
||||
async () => this.brushSettings.type
|
||||
)
|
||||
this.messageBroker.createPullTopic(
|
||||
'brushSmoothingPrecision',
|
||||
async () => this.brushSettings.smoothingPrecision
|
||||
)
|
||||
this.messageBroker.createPullTopic(
|
||||
'maskBlendMode',
|
||||
async () => this.maskBlendMode
|
||||
@@ -2184,7 +2143,7 @@ class BrushTool {
|
||||
}
|
||||
|
||||
const distanceBetweenPoints =
|
||||
(this.brushSettings.size / this.brushSettings.smoothingPrecision) * 6
|
||||
(this.brushSettings.size / this.smoothingPrecision) * 6
|
||||
const stepNr = Math.ceil(totalLength / distanceBetweenPoints)
|
||||
|
||||
let interpolatedPoints = points
|
||||
@@ -2231,7 +2190,7 @@ class BrushTool {
|
||||
const brush_size = await this.messageBroker.pull('brushSize')
|
||||
const distance = Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2)
|
||||
const steps = Math.ceil(
|
||||
distance / ((brush_size / this.brushSettings.smoothingPrecision) * 4)
|
||||
distance / ((brush_size / this.smoothingPrecision) * 4)
|
||||
) // Adjust for smoother lines
|
||||
const interpolatedOpacity =
|
||||
1 / (1 + Math.exp(-6 * (this.brushSettings.opacity - 0.5))) -
|
||||
@@ -2586,27 +2545,23 @@ class BrushTool {
|
||||
|
||||
private setBrushSize(size: number) {
|
||||
this.brushSettings.size = size
|
||||
saveBrushToCache('maskeditor_brush_settings', this.brushSettings)
|
||||
}
|
||||
|
||||
private setBrushOpacity(opacity: number) {
|
||||
this.brushSettings.opacity = opacity
|
||||
saveBrushToCache('maskeditor_brush_settings', this.brushSettings)
|
||||
}
|
||||
|
||||
private setBrushHardness(hardness: number) {
|
||||
this.brushSettings.hardness = hardness
|
||||
saveBrushToCache('maskeditor_brush_settings', this.brushSettings)
|
||||
}
|
||||
|
||||
private setBrushType(type: BrushShape) {
|
||||
this.brushSettings.type = type
|
||||
saveBrushToCache('maskeditor_brush_settings', this.brushSettings)
|
||||
}
|
||||
|
||||
private setBrushSmoothingPrecision(precision: number) {
|
||||
this.brushSettings.smoothingPrecision = precision
|
||||
saveBrushToCache('maskeditor_brush_settings', this.brushSettings)
|
||||
//console.log('precision', precision)
|
||||
this.smoothingPrecision = precision
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2870,6 +2825,7 @@ class UIManager {
|
||||
const circle_shape = document.createElement('div')
|
||||
circle_shape.id = 'maskEditor_sidePanelBrushShapeCircle'
|
||||
circle_shape.classList.add(shapeColor)
|
||||
circle_shape.style.background = 'var(--p-button-text-primary-color)'
|
||||
circle_shape.addEventListener('click', () => {
|
||||
this.messageBroker.publish('setBrushShape', BrushShape.Arc)
|
||||
this.setBrushBorderRadius()
|
||||
@@ -2880,6 +2836,7 @@ class UIManager {
|
||||
const square_shape = document.createElement('div')
|
||||
square_shape.id = 'maskEditor_sidePanelBrushShapeSquare'
|
||||
square_shape.classList.add(shapeColor)
|
||||
square_shape.style.background = ''
|
||||
square_shape.addEventListener('click', () => {
|
||||
this.messageBroker.publish('setBrushShape', BrushShape.Rect)
|
||||
this.setBrushBorderRadius()
|
||||
@@ -2887,16 +2844,6 @@ class UIManager {
|
||||
circle_shape.style.background = ''
|
||||
})
|
||||
|
||||
if (
|
||||
(await this.messageBroker.pull('brushSettings')).type === BrushShape.Arc
|
||||
) {
|
||||
circle_shape.style.background = 'var(--p-button-text-primary-color)'
|
||||
square_shape.style.background = ''
|
||||
} else {
|
||||
circle_shape.style.background = ''
|
||||
square_shape.style.background = 'var(--p-button-text-primary-color)'
|
||||
}
|
||||
|
||||
brush_shape_container.appendChild(circle_shape)
|
||||
brush_shape_container.appendChild(square_shape)
|
||||
|
||||
@@ -2908,7 +2855,7 @@ class UIManager {
|
||||
1,
|
||||
100,
|
||||
1,
|
||||
(await this.messageBroker.pull('brushSettings')).size,
|
||||
10,
|
||||
(event, value) => {
|
||||
this.messageBroker.publish('setBrushSize', parseInt(value))
|
||||
this.updateBrushPreview()
|
||||
@@ -2921,7 +2868,7 @@ class UIManager {
|
||||
0,
|
||||
1,
|
||||
0.01,
|
||||
(await this.messageBroker.pull('brushSettings')).opacity,
|
||||
0.7,
|
||||
(event, value) => {
|
||||
this.messageBroker.publish('setBrushOpacity', parseFloat(value))
|
||||
this.updateBrushPreview()
|
||||
@@ -2934,7 +2881,7 @@ class UIManager {
|
||||
0,
|
||||
1,
|
||||
0.01,
|
||||
(await this.messageBroker.pull('brushSettings')).hardness,
|
||||
1,
|
||||
(event, value) => {
|
||||
this.messageBroker.publish('setBrushHardness', parseFloat(value))
|
||||
this.updateBrushPreview()
|
||||
@@ -2947,7 +2894,7 @@ class UIManager {
|
||||
1,
|
||||
100,
|
||||
1,
|
||||
(await this.messageBroker.pull('brushSettings')).smoothingPrecision,
|
||||
10,
|
||||
(event, value) => {
|
||||
this.messageBroker.publish(
|
||||
'setBrushSmoothingPrecision',
|
||||
@@ -2956,31 +2903,7 @@ class UIManager {
|
||||
}
|
||||
)
|
||||
|
||||
const resetBrushSettingsButton = document.createElement('button')
|
||||
resetBrushSettingsButton.id = 'resetBrushSettingsButton'
|
||||
resetBrushSettingsButton.innerText = 'Reset to Default'
|
||||
|
||||
resetBrushSettingsButton.addEventListener('click', () => {
|
||||
this.messageBroker.publish('setBrushShape', BrushShape.Arc)
|
||||
this.messageBroker.publish('setBrushSize', 10)
|
||||
this.messageBroker.publish('setBrushOpacity', 0.7)
|
||||
this.messageBroker.publish('setBrushHardness', 1)
|
||||
this.messageBroker.publish('setBrushSmoothingPrecision', 10)
|
||||
|
||||
circle_shape.style.background = 'var(--p-button-text-primary-color)'
|
||||
square_shape.style.background = ''
|
||||
|
||||
thicknesSliderObj.slider.value = '10'
|
||||
opacitySliderObj.slider.value = '0.7'
|
||||
hardnessSliderObj.slider.value = '1'
|
||||
brushSmoothingPrecisionSliderObj.slider.value = '10'
|
||||
|
||||
this.setBrushBorderRadius()
|
||||
this.updateBrushPreview()
|
||||
})
|
||||
|
||||
brush_settings_container.appendChild(brush_settings_title)
|
||||
brush_settings_container.appendChild(resetBrushSettingsButton)
|
||||
brush_settings_container.appendChild(brush_shape_outer_container)
|
||||
brush_settings_container.appendChild(thicknesSliderObj.container)
|
||||
brush_settings_container.appendChild(opacitySliderObj.container)
|
||||
|
||||
@@ -782,27 +782,7 @@ app.registerExtension({
|
||||
? origGetExtraMenuOptions.apply(this, arguments)
|
||||
: undefined
|
||||
|
||||
const getPointerCanvasPos = () => {
|
||||
const pos = this.graph?.list_of_graphcanvas?.at(0)?.graph_mouse
|
||||
return pos ? { canvasX: pos[0], canvasY: pos[1] } : undefined
|
||||
}
|
||||
|
||||
if (this.widgets) {
|
||||
const { canvasX, canvasY } = getPointerCanvasPos()
|
||||
const widget = this.getWidgetOnPos(canvasX, canvasY)
|
||||
// @ts-expect-error custom widget type
|
||||
if (widget && widget.type !== CONVERTED_TYPE) {
|
||||
const config = getConfig.call(this, widget.name) ?? [
|
||||
widget.type,
|
||||
widget.options || {}
|
||||
]
|
||||
if (isConvertibleWidget(widget, config)) {
|
||||
options.push({
|
||||
content: `Convert ${widget.name} to input`,
|
||||
callback: () => convertToInput(this, widget, config) && false
|
||||
})
|
||||
}
|
||||
}
|
||||
let toInput = []
|
||||
let toWidget = []
|
||||
for (const w of this.widgets) {
|
||||
|
||||
@@ -36,21 +36,12 @@ export function useTerminal(element: Ref<HTMLElement>) {
|
||||
|
||||
return {
|
||||
terminal,
|
||||
useAutoSize({
|
||||
root,
|
||||
autoRows = true,
|
||||
autoCols = true,
|
||||
minCols = Number.NEGATIVE_INFINITY,
|
||||
minRows = Number.NEGATIVE_INFINITY,
|
||||
onResize
|
||||
}: {
|
||||
root: Ref<HTMLElement>
|
||||
autoRows?: boolean
|
||||
autoCols?: boolean
|
||||
minCols?: number
|
||||
minRows?: number
|
||||
useAutoSize(
|
||||
root: Ref<HTMLElement>,
|
||||
autoRows: boolean = true,
|
||||
autoCols: boolean = true,
|
||||
onResize?: () => void
|
||||
}) {
|
||||
) {
|
||||
const ensureValidRows = (rows: number | undefined) => {
|
||||
if (rows == null || isNaN(rows)) {
|
||||
return root.value?.clientHeight / 20
|
||||
@@ -70,14 +61,8 @@ export function useTerminal(element: Ref<HTMLElement>) {
|
||||
const dims = fitAddon.proposeDimensions()
|
||||
// Sometimes propose returns NaN, so we may need to estimate.
|
||||
terminal.resize(
|
||||
Math.max(
|
||||
autoCols ? ensureValidCols(dims?.cols) : terminal.cols,
|
||||
minCols
|
||||
),
|
||||
Math.max(
|
||||
autoRows ? ensureValidRows(dims?.rows) : terminal.rows,
|
||||
minRows
|
||||
)
|
||||
autoCols ? ensureValidCols(dims?.cols) : terminal.cols,
|
||||
autoRows ? ensureValidRows(dims?.rows) : terminal.rows
|
||||
)
|
||||
onResize?.()
|
||||
}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import { Ref } from 'vue'
|
||||
|
||||
import { usePragmaticDroppable } from '@/hooks/dndHooks'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { ComfyModelDef } from '@/stores/modelStore'
|
||||
import { ModelNodeProvider } from '@/stores/modelToNodeStore'
|
||||
import { useModelToNodeStore } from '@/stores/modelToNodeStore'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
|
||||
export const useCanvasDrop = (canvasRef: Ref<HTMLCanvasElement>) => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
const litegraphService = useLitegraphService()
|
||||
|
||||
usePragmaticDroppable(() => canvasRef.value, {
|
||||
getDropEffect: (args): Exclude<DataTransfer['dropEffect'], 'none'> =>
|
||||
args.source.data.type === 'tree-explorer-node' ? 'copy' : 'move',
|
||||
onDrop: (event) => {
|
||||
const loc = event.location.current.input
|
||||
const dndData = event.source.data
|
||||
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
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
|
||||
// is on the node (top left corner)
|
||||
const pos = comfyApp.clientPosToCanvasPos([
|
||||
loc.clientX,
|
||||
loc.clientY + LiteGraph.NODE_TITLE_HEIGHT
|
||||
])
|
||||
litegraphService.addNodeOnGraph(nodeDef, { pos })
|
||||
} else if (node.data instanceof ComfyModelDef) {
|
||||
const model = node.data
|
||||
const pos = comfyApp.clientPosToCanvasPos([loc.clientX, loc.clientY])
|
||||
const nodeAtPos = comfyApp.graph.getNodeOnPos(pos[0], pos[1])
|
||||
let targetProvider: ModelNodeProvider | null = null
|
||||
let targetGraphNode: LGraphNode | null = null
|
||||
if (nodeAtPos) {
|
||||
const providers = modelToNodeStore.getAllNodeProviders(
|
||||
model.directory
|
||||
)
|
||||
for (const provider of providers) {
|
||||
if (provider.nodeDef.name === nodeAtPos.comfyClass) {
|
||||
targetGraphNode = nodeAtPos
|
||||
targetProvider = provider
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!targetGraphNode) {
|
||||
const provider = modelToNodeStore.getNodeProvider(model.directory)
|
||||
if (provider) {
|
||||
targetGraphNode = litegraphService.addNodeOnGraph(
|
||||
provider.nodeDef,
|
||||
{
|
||||
pos
|
||||
}
|
||||
)
|
||||
targetProvider = provider
|
||||
}
|
||||
}
|
||||
if (targetGraphNode) {
|
||||
const widget = targetGraphNode.widgets?.find(
|
||||
(widget) => widget.name === targetProvider?.key
|
||||
)
|
||||
if (widget) {
|
||||
widget.value = model.file_name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
DEFAULT_DARK_COLOR_PALETTE,
|
||||
DEFAULT_LIGHT_COLOR_PALETTE
|
||||
} from '@/constants/coreColorPalettes'
|
||||
import { t } from '@/i18n'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
@@ -539,32 +538,6 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
if (workflowStore.activeWorkflow)
|
||||
workflowService.closeWorkflow(workflowStore.activeWorkflow)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Feedback',
|
||||
icon: 'pi pi-megaphone',
|
||||
label: 'Give Feedback',
|
||||
versionAdded: '1.8.2',
|
||||
function: () => {
|
||||
dialogService.showIssueReportDialog({
|
||||
title: t('g.feedback'),
|
||||
subtitle: t('issueReport.feedbackTitle'),
|
||||
panelProps: {
|
||||
errorType: 'Feedback',
|
||||
defaultFields: ['SystemStats', 'Settings']
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Help.OpenComfyUIForum',
|
||||
icon: 'pi pi-comments',
|
||||
label: 'Open ComfyUI Forum',
|
||||
menubarLabel: 'ComfyUI Forum',
|
||||
versionAdded: '1.8.2',
|
||||
function: () => {
|
||||
window.open('https://forum.comfy.org/', '_blank')
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
ContextMenu,
|
||||
DragAndScale,
|
||||
LGraph,
|
||||
LGraphBadge,
|
||||
LGraphCanvas,
|
||||
LGraphGroup,
|
||||
LGraphNode,
|
||||
LLink,
|
||||
LiteGraph
|
||||
} from '@comfyorg/litegraph'
|
||||
|
||||
/**
|
||||
* Assign all properties of LiteGraph to window to make it backward compatible.
|
||||
*/
|
||||
export const useGlobalLitegraph = () => {
|
||||
window['LiteGraph'] = LiteGraph
|
||||
window['LGraph'] = LGraph
|
||||
window['LLink'] = LLink
|
||||
window['LGraphNode'] = LGraphNode
|
||||
window['LGraphGroup'] = LGraphGroup
|
||||
window['DragAndScale'] = DragAndScale
|
||||
window['LGraphCanvas'] = LGraphCanvas
|
||||
window['ContextMenu'] = ContextMenu
|
||||
window['LGraphBadge'] = LGraphBadge
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
import { computed, watch, watchEffect } from 'vue'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { getStorageValue, setStorageValue } from '@/scripts/utils'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import { useModelStore } from '@/stores/modelStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
|
||||
export function useWorkflowPersistence() {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const persistCurrentWorkflow = () => {
|
||||
const workflow = JSON.stringify(comfyApp.serializeGraph())
|
||||
localStorage.setItem('workflow', workflow)
|
||||
if (api.clientId) {
|
||||
sessionStorage.setItem(`workflow:${api.clientId}`, workflow)
|
||||
}
|
||||
}
|
||||
|
||||
const loadWorkflowFromStorage = async (
|
||||
json: string | null,
|
||||
workflowName: string | null
|
||||
) => {
|
||||
if (!json) return false
|
||||
const workflow = JSON.parse(json)
|
||||
await comfyApp.loadGraphData(workflow, true, true, workflowName)
|
||||
return true
|
||||
}
|
||||
|
||||
const loadPreviousWorkflowFromStorage = async () => {
|
||||
const workflowName = getStorageValue('Comfy.PreviousWorkflow')
|
||||
const clientId = api.initialClientId ?? api.clientId
|
||||
|
||||
// Try loading from session storage first
|
||||
if (clientId) {
|
||||
const sessionWorkflow = sessionStorage.getItem(`workflow:${clientId}`)
|
||||
if (await loadWorkflowFromStorage(sessionWorkflow, workflowName)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to local storage
|
||||
const localWorkflow = localStorage.getItem('workflow')
|
||||
return await loadWorkflowFromStorage(localWorkflow, workflowName)
|
||||
}
|
||||
|
||||
const loadDefaultWorkflow = async () => {
|
||||
if (!settingStore.get('Comfy.TutorialCompleted')) {
|
||||
await settingStore.set('Comfy.TutorialCompleted', true)
|
||||
await useModelStore().loadModelFolders()
|
||||
await useWorkflowService().loadTutorialWorkflow()
|
||||
} else {
|
||||
await comfyApp.loadGraphData()
|
||||
}
|
||||
}
|
||||
|
||||
const restorePreviousWorkflow = async () => {
|
||||
try {
|
||||
const restored = await loadPreviousWorkflowFromStorage()
|
||||
if (!restored) {
|
||||
await loadDefaultWorkflow()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading previous workflow', err)
|
||||
await loadDefaultWorkflow()
|
||||
}
|
||||
}
|
||||
|
||||
// Setup watchers
|
||||
watchEffect(() => {
|
||||
if (workflowStore.activeWorkflow) {
|
||||
const workflow = workflowStore.activeWorkflow
|
||||
setStorageValue('Comfy.PreviousWorkflow', workflow.key)
|
||||
// When the activeWorkflow changes, the graph has already been loaded.
|
||||
// Saving the current state of the graph to the localStorage.
|
||||
persistCurrentWorkflow()
|
||||
}
|
||||
})
|
||||
api.addEventListener('graphChanged', persistCurrentWorkflow)
|
||||
|
||||
// Restore workflow tabs states
|
||||
const openWorkflows = computed(() => workflowStore.openWorkflows)
|
||||
const activeWorkflow = computed(() => workflowStore.activeWorkflow)
|
||||
const restoreState = computed<{ paths: string[]; activeIndex: number }>(
|
||||
() => {
|
||||
if (!openWorkflows.value || !activeWorkflow.value) {
|
||||
return { paths: [], activeIndex: -1 }
|
||||
}
|
||||
|
||||
const paths = openWorkflows.value
|
||||
.filter((workflow) => workflow?.isPersisted && !workflow.isModified)
|
||||
.map((workflow) => workflow.path)
|
||||
const activeIndex = openWorkflows.value.findIndex(
|
||||
(workflow) => workflow.path === activeWorkflow.value?.path
|
||||
)
|
||||
|
||||
return { paths, activeIndex }
|
||||
}
|
||||
)
|
||||
|
||||
// Get storage values before setting watchers
|
||||
const storedWorkflows = JSON.parse(
|
||||
getStorageValue('Comfy.OpenWorkflowsPaths') || '[]'
|
||||
)
|
||||
const storedActiveIndex = JSON.parse(
|
||||
getStorageValue('Comfy.ActiveWorkflowIndex') || '-1'
|
||||
)
|
||||
watch(restoreState, ({ paths, activeIndex }) => {
|
||||
setStorageValue('Comfy.OpenWorkflowsPaths', JSON.stringify(paths))
|
||||
setStorageValue('Comfy.ActiveWorkflowIndex', JSON.stringify(activeIndex))
|
||||
})
|
||||
|
||||
const restoreWorkflowTabsState = () => {
|
||||
const isRestorable = storedWorkflows?.length > 0 && storedActiveIndex >= 0
|
||||
if (isRestorable) {
|
||||
workflowStore.openWorkflowsInBackground({
|
||||
left: storedWorkflows.slice(0, storedActiveIndex),
|
||||
right: storedWorkflows.slice(storedActiveIndex)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
restorePreviousWorkflow,
|
||||
restoreWorkflowTabsState
|
||||
}
|
||||
}
|
||||
@@ -20,12 +20,12 @@
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "Open DevTools"
|
||||
},
|
||||
"Comfy-Desktop_OpenFeedbackPage": {
|
||||
"label": "Feedback"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "Desktop User Guide"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "Quit"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "Reinstall"
|
||||
},
|
||||
@@ -83,9 +83,6 @@
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "Export Workflow (API Format)"
|
||||
},
|
||||
"Comfy_Feedback": {
|
||||
"label": "Give Feedback"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "Fit Group To Contents"
|
||||
},
|
||||
@@ -110,9 +107,6 @@
|
||||
"Comfy_Help_OpenComfyUIDocs": {
|
||||
"label": "Open ComfyUI Docs"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIForum": {
|
||||
"label": "Open ComfyUI Forum"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIIssues": {
|
||||
"label": "Open ComfyUI Issues"
|
||||
},
|
||||
|
||||
@@ -70,26 +70,17 @@
|
||||
"keybinding": "Keybinding",
|
||||
"upload": "Upload",
|
||||
"export": "Export",
|
||||
"workflow": "Workflow",
|
||||
"success": "Success",
|
||||
"ok": "OK",
|
||||
"feedback": "Feedback"
|
||||
"workflow": "Workflow"
|
||||
},
|
||||
"issueReport": {
|
||||
"submitErrorReport": "Submit Error Report (Optional)",
|
||||
"provideEmail": "Give us your email (optional)",
|
||||
"provideEmail": "Give us your email (Optional)",
|
||||
"provideAdditionalDetails": "Provide additional details (optional)",
|
||||
"stackTrace": "Stack Trace",
|
||||
"systemStats": "System Stats",
|
||||
"contactFollowUp": "Contact me for follow up",
|
||||
"notifyResolve": "Notify me when resolved",
|
||||
"helpFix": "Help Fix This",
|
||||
"rating": "Rating",
|
||||
"feedbackTitle": "Help us improve ComfyUI by providing feedback",
|
||||
"validation": {
|
||||
"maxLength": "Message too long",
|
||||
"invalidEmail": "Please enter a valid email address"
|
||||
}
|
||||
"helpFix": "Help Fix This"
|
||||
},
|
||||
"color": {
|
||||
"default": "Default",
|
||||
@@ -160,7 +151,7 @@
|
||||
"appDataLocationTooltip": "ComfyUI's app data directory. Stores:\n- Logs\n- Server configs",
|
||||
"appPathLocationTooltip": "ComfyUI's app asset directory. Stores the ComfyUI code and assets",
|
||||
"migrateFromExistingInstallation": "Migrate from Existing Installation",
|
||||
"migrationSourcePathDescription": "If you have an existing ComfyUI installation, we can copy/link your existing user files and models to the new installation. Your existing ComfyUI installation will not be affected.",
|
||||
"migrationSourcePathDescription": "If you have an existing ComfyUI installation, we can copy/link your existing user files and models to the new installation.",
|
||||
"selectItemsToMigrate": "Select Items to Migrate",
|
||||
"migrationOptional": "Migration is optional. If you don't have an existing installation, you can skip this step.",
|
||||
"desktopAppSettings": "Desktop App Settings",
|
||||
@@ -191,8 +182,6 @@
|
||||
"settings": {
|
||||
"autoUpdate": "Automatic Updates",
|
||||
"allowMetrics": "Usage Metrics",
|
||||
"errorUpdatingConsent": "Error Updating Consent",
|
||||
"errorUpdatingConsentDetail": "Failed to update metrics consent settings",
|
||||
"autoUpdateDescription": "Automatically download updates when they become available. You will be notified before updates are installed.",
|
||||
"allowMetricsDescription": "Help improve ComfyUI by sending anonymous usage metrics. No personal information or workflow content will be collected.",
|
||||
"learnMoreAboutData": "Learn more about data collection",
|
||||
@@ -203,31 +192,19 @@
|
||||
"collect": {
|
||||
"errorReports": "Error message and stack trace",
|
||||
"systemInfo": "Hardware, OS type, and app version",
|
||||
"userJourneyEvents": "User journey events"
|
||||
"userJourneyEvents": "User journey events",
|
||||
"userJourneyTooltip": "User journey events are used to track the user's journey through the app installation process. The event collection ends on the first successful ComfyUI workflow run."
|
||||
},
|
||||
"doNotCollect": {
|
||||
"personalInformation": "Personal information",
|
||||
"fileSystemInformation": "File system information",
|
||||
"workflowContents": "Workflow contents",
|
||||
"customNodeConfigurations": "Custom node configurations"
|
||||
},
|
||||
"viewFullPolicy": "View full policy"
|
||||
},
|
||||
"pythonMirrorPlaceholder": "Enter Python mirror URL",
|
||||
"pypiMirrorPlaceholder": "Enter PyPI mirror URL",
|
||||
"checkingMirrors": "Checking network access to python mirrors...",
|
||||
"mirrorsReachable": "Network access to python mirrors is good",
|
||||
"mirrorsUnreachable": "Network access to some python mirrors is bad",
|
||||
"mirrorSettings": "Mirror Settings"
|
||||
}
|
||||
}
|
||||
},
|
||||
"customNodes": "Custom Nodes",
|
||||
"customNodesDescription": "Reinstall custom nodes from existing ComfyUI installations.",
|
||||
"helpImprove": "Please help improve ComfyUI",
|
||||
"moreInfo": "For more info, please read our",
|
||||
"privacyPolicy": "privacy policy",
|
||||
"metricsEnabled": "Metrics Enabled",
|
||||
"metricsDisabled": "Metrics Disabled",
|
||||
"updateConsent": "You previously opted in to reporting crashes. We are now tracking event-based metrics to help identify bugs and improve the app. No personal identifiable information is collected."
|
||||
"customNodesDescription": "Reinstall custom nodes from existing ComfyUI installations."
|
||||
},
|
||||
"serverStart": {
|
||||
"reinstall": "Reinstall",
|
||||
@@ -257,7 +234,8 @@
|
||||
"openWorkflow": "Open workflow in local file system",
|
||||
"newBlankWorkflow": "Create a new blank workflow",
|
||||
"nodeLibraryTab": {
|
||||
"sortOrder": "Sort Order"
|
||||
"sortOrder": "Sort Order",
|
||||
"groupingType": "Grouping Type"
|
||||
},
|
||||
"modelLibrary": "Model Library",
|
||||
"downloads": "Downloads",
|
||||
@@ -372,8 +350,8 @@
|
||||
"Open Models Folder": "Open Models Folder",
|
||||
"Open Outputs Folder": "Open Outputs Folder",
|
||||
"Open DevTools": "Open DevTools",
|
||||
"Feedback": "Feedback",
|
||||
"Desktop User Guide": "Desktop User Guide",
|
||||
"Quit": "Quit",
|
||||
"Reinstall": "Reinstall",
|
||||
"Restart": "Restart",
|
||||
"Browse Templates": "Browse Templates",
|
||||
@@ -393,7 +371,6 @@
|
||||
"Duplicate Current Workflow": "Duplicate Current Workflow",
|
||||
"Export": "Export",
|
||||
"Export (API)": "Export (API)",
|
||||
"Give Feedback": "Give Feedback",
|
||||
"Fit Group To Contents": "Fit Group To Contents",
|
||||
"Group Selected Nodes": "Group Selected Nodes",
|
||||
"Convert selected nodes to group node": "Convert selected nodes to group node",
|
||||
@@ -402,7 +379,6 @@
|
||||
"About ComfyUI": "About ComfyUI",
|
||||
"Comfy-Org Discord": "Comfy-Org Discord",
|
||||
"ComfyUI Docs": "ComfyUI Docs",
|
||||
"ComfyUI Forum": "ComfyUI Forum",
|
||||
"ComfyUI Issues": "ComfyUI Issues",
|
||||
"Interrupt": "Interrupt",
|
||||
"Load Default Workflow": "Load Default Workflow",
|
||||
@@ -433,9 +409,7 @@
|
||||
},
|
||||
"desktopMenu": {
|
||||
"reinstall": "Reinstall",
|
||||
"confirmReinstall": "This will clear your extra_models_config.yaml file,\nand begin installation again.\n\nAre you sure?",
|
||||
"quit": "Quit",
|
||||
"confirmQuit": "There are unsaved workflows open; any unsaved changes will be lost. Ignore this and quit?"
|
||||
"confirmReinstall": "This will clear your extra_models_config.yaml file,\nand begin installation again.\n\nAre you sure?"
|
||||
},
|
||||
"settingsCategories": {
|
||||
"Comfy-Desktop": "Comfy-Desktop",
|
||||
@@ -477,9 +451,7 @@
|
||||
"Server-Config": "Server-Config",
|
||||
"About": "About",
|
||||
"EditTokenWeight": "Edit Token Weight",
|
||||
"CustomColorPalettes": "Custom Color Palettes",
|
||||
"UV": "UV",
|
||||
"ContextMenu": "Context Menu"
|
||||
"CustomColorPalettes": "Custom Color Palettes"
|
||||
},
|
||||
"serverConfigItems": {
|
||||
"listen": {
|
||||
@@ -629,7 +601,6 @@
|
||||
"combine": "combine",
|
||||
"cond single": "cond single",
|
||||
"controlnet": "controlnet",
|
||||
"inpaint": "inpaint",
|
||||
"scheduling": "scheduling",
|
||||
"create": "create",
|
||||
"mask": "mask",
|
||||
@@ -649,6 +620,7 @@
|
||||
"batch": "batch",
|
||||
"video_models": "video_models",
|
||||
"upscaling": "upscaling",
|
||||
"inpaint": "inpaint",
|
||||
"instructpix2pix": "instructpix2pix",
|
||||
"compositing": "compositing",
|
||||
"samplers": "samplers",
|
||||
@@ -700,26 +672,5 @@
|
||||
"UPSCALE_MODEL": "UPSCALE_MODEL",
|
||||
"VAE": "VAE",
|
||||
"WEBCAM": "WEBCAM"
|
||||
},
|
||||
"maintenance": {
|
||||
"allOk": "No issues were detected.",
|
||||
"status": "Status",
|
||||
"detected": "Detected",
|
||||
"refreshing": "Refreshing",
|
||||
"None": "None",
|
||||
"OK": "OK",
|
||||
"Skipped": "Skipped",
|
||||
"showManual": "Show maintenance tasks",
|
||||
"confirmTitle": "Are you sure?",
|
||||
"error": {
|
||||
"toastTitle": "Task error",
|
||||
"taskFailed": "Task failed to run.",
|
||||
"defaultDescription": "An error occurred while running a maintenance task."
|
||||
}
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "Don't show this again",
|
||||
"missingModels": "Missing Models",
|
||||
"missingModelsMessage": "When loading the graph, the following models were not found"
|
||||
}
|
||||
}
|
||||
@@ -177,7 +177,7 @@
|
||||
},
|
||||
"CLIPLoader": {
|
||||
"display_name": "Load CLIP",
|
||||
"description": "[Recipes]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 / clip-g / clip-l\nstable_audio: t5\nmochi: t5\ncosmos: old t5 xxl",
|
||||
"description": "[Recipes]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 / clip-g / clip-l\nstable_audio: t5\nmochi: t5",
|
||||
"inputs": {
|
||||
"clip_name": {
|
||||
"name": "clip_name"
|
||||
@@ -862,32 +862,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CosmosImageToVideoLatent": {
|
||||
"display_name": "CosmosImageToVideoLatent",
|
||||
"inputs": {
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
},
|
||||
"width": {
|
||||
"name": "width"
|
||||
},
|
||||
"height": {
|
||||
"name": "height"
|
||||
},
|
||||
"length": {
|
||||
"name": "length"
|
||||
},
|
||||
"batch_size": {
|
||||
"name": "batch_size"
|
||||
},
|
||||
"start_image": {
|
||||
"name": "start_image"
|
||||
},
|
||||
"end_image": {
|
||||
"name": "end_image"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateHookKeyframe": {
|
||||
"display_name": "Create Hook Keyframe",
|
||||
"inputs": {
|
||||
@@ -1256,23 +1230,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmptyCosmosLatentVideo": {
|
||||
"display_name": "EmptyCosmosLatentVideo",
|
||||
"inputs": {
|
||||
"width": {
|
||||
"name": "width"
|
||||
},
|
||||
"height": {
|
||||
"name": "height"
|
||||
},
|
||||
"length": {
|
||||
"name": "length"
|
||||
},
|
||||
"batch_size": {
|
||||
"name": "batch_size"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmptyHunyuanLatentVideo": {
|
||||
"display_name": "EmptyHunyuanLatentVideo",
|
||||
"inputs": {
|
||||
@@ -4788,17 +4745,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SetFirstSigma": {
|
||||
"display_name": "SetFirstSigma",
|
||||
"inputs": {
|
||||
"sigmas": {
|
||||
"name": "sigmas"
|
||||
},
|
||||
"sigma": {
|
||||
"name": "sigma"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SetHookKeyframes": {
|
||||
"display_name": "Set Hook Keyframes",
|
||||
"inputs": {
|
||||
|
||||
@@ -5,21 +5,9 @@
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "Send anonymous usage metrics"
|
||||
},
|
||||
"Comfy-Desktop_UV_PypiInstallMirror": {
|
||||
"name": "Pypi Install Mirror",
|
||||
"tooltip": "Default pip install mirror"
|
||||
},
|
||||
"Comfy-Desktop_UV_PythonInstallMirror": {
|
||||
"name": "Python Install Mirror",
|
||||
"tooltip": "Managed Python installations are downloaded from the Astral python-build-standalone project. This variable can be set to a mirror URL to use a different source for Python installations. The provided URL will replace https://github.com/astral-sh/python-build-standalone/releases/download in, e.g., https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz. Distributions can be read from a local directory by using the file:// URL scheme."
|
||||
},
|
||||
"Comfy-Desktop_UV_TorchInstallMirror": {
|
||||
"name": "Torch Install Mirror",
|
||||
"tooltip": "Pip install mirror for pytorch"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "Window Style",
|
||||
"tooltip": "Custom: Replace the system title bar with ComfyUI's Top menu",
|
||||
"tooltip": "Choose custom option to hide the system title bar",
|
||||
"options": {
|
||||
"default": "default",
|
||||
"custom": "custom"
|
||||
@@ -321,9 +309,6 @@
|
||||
"name": "Maxium FPS",
|
||||
"tooltip": "The maximum frames per second that the canvas is allowed to render. Caps GPU usage at the cost of smoothness. If 0, the screen refresh rate is used. Default: 0"
|
||||
},
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "Scale node combo widget menus (lists) when zoomed in"
|
||||
},
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "Always snap to grid"
|
||||
}
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "Ouvrir les outils de développement"
|
||||
},
|
||||
"Comfy-Desktop_OpenFeedbackPage": {
|
||||
"label": "Retour d'information"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "Guide de l'utilisateur du bureau"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "Quitter"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "Réinstaller"
|
||||
},
|
||||
@@ -83,9 +83,6 @@
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "Exporter le flux de travail (format API)"
|
||||
},
|
||||
"Comfy_Feedback": {
|
||||
"label": "Retour d'information"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "Ajuster le groupe au contenu"
|
||||
},
|
||||
@@ -110,9 +107,6 @@
|
||||
"Comfy_Help_OpenComfyUIDocs": {
|
||||
"label": "Ouvrir les documents ComfyUI"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIForum": {
|
||||
"label": "Ouvrir le forum Comfy-Org"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIIssues": {
|
||||
"label": "Ouvrir les problèmes ComfyUI"
|
||||
},
|
||||
|
||||
@@ -43,9 +43,7 @@
|
||||
"WEBCAM": "WEBCAM"
|
||||
},
|
||||
"desktopMenu": {
|
||||
"confirmQuit": "Il y a des flux de travail non enregistrés ouverts; toutes les modifications non enregistrées seront perdues. Ignorer cela et quitter?",
|
||||
"confirmReinstall": "Cela effacera votre fichier extra_models_config.yaml,\net commencera l'installation à nouveau.\n\nÊtes-vous sûr ?",
|
||||
"quit": "Quitter",
|
||||
"reinstall": "Réinstaller"
|
||||
},
|
||||
"downloadGit": {
|
||||
@@ -89,7 +87,6 @@
|
||||
"experimental": "BETA",
|
||||
"export": "Exportation",
|
||||
"extensionName": "Nom de l'extension",
|
||||
"feedback": "Commentaires",
|
||||
"findIssues": "Trouver des problèmes",
|
||||
"firstTimeUIMessage": "C'est la première fois que vous utilisez la nouvelle interface utilisateur. Choisissez \"Menu > Utiliser le nouveau menu > Désactivé\" pour restaurer l'ancienne interface utilisateur.",
|
||||
"goToNode": "Aller au nœud",
|
||||
@@ -110,7 +107,6 @@
|
||||
"noTasksFound": "Aucune tâche trouvée",
|
||||
"noTasksFoundMessage": "Il n'y a pas de tâches dans la file d'attente.",
|
||||
"noWorkflowsFound": "Aucun flux de travail trouvé.",
|
||||
"ok": "OK",
|
||||
"openNewIssue": "Ouvrir un nouveau problème",
|
||||
"overwrite": "Écraser",
|
||||
"reconnected": "Reconnecté",
|
||||
@@ -133,7 +129,6 @@
|
||||
"searchWorkflows": "Rechercher des flux de travail",
|
||||
"settings": "Paramètres",
|
||||
"showReport": "Afficher le rapport",
|
||||
"success": "Succès",
|
||||
"systemInfo": "Informations système",
|
||||
"terminal": "Terminal",
|
||||
"upload": "Téléverser",
|
||||
@@ -191,7 +186,6 @@
|
||||
"selectGpu": "Sélectionnez le GPU",
|
||||
"selectGpuDescription": "Sélectionnez le type de GPU que vous avez"
|
||||
},
|
||||
"helpImprove": "Veuillez aider à améliorer ComfyUI",
|
||||
"installLocation": "Emplacement d'installation",
|
||||
"installLocationDescription": "Sélectionnez le répertoire pour les données utilisateur de ComfyUI. Un environnement python sera installé à l'emplacement sélectionné. Veuillez vous assurer que le disque sélectionné a suffisamment d'espace (~15GB) restant.",
|
||||
"installLocationTooltip": "Répertoire des données utilisateur de ComfyUI. Stocke :\n- Environnement Python\n- Modèles\n- Nœuds personnalisés\n",
|
||||
@@ -203,29 +197,25 @@
|
||||
"title": "Configuration manuelle",
|
||||
"virtualEnvironmentPath": "Chemin de l'environnement virtuel"
|
||||
},
|
||||
"metricsDisabled": "Métriques désactivées",
|
||||
"metricsEnabled": "Métriques activées",
|
||||
"migrateFromExistingInstallation": "Migrer à partir d'une installation existante",
|
||||
"migration": "Migration",
|
||||
"migrationOptional": "La migration est facultative. Si vous n'avez pas d'installation existante, vous pouvez sauter cette étape.",
|
||||
"migrationSourcePathDescription": "Si vous avez une installation existante de ComfyUI, nous pouvons copier/lier vos fichiers utilisateur et modèles existants à la nouvelle installation. Votre installation existante de ComfyUI ne sera pas affectée.",
|
||||
"moreInfo": "Pour plus d'informations, veuillez lire notre",
|
||||
"migrationSourcePathDescription": "Si vous avez une installation existante de ComfyUI, nous pouvons copier/lier vos fichiers utilisateur et modèles existants à la nouvelle installation.",
|
||||
"parentMissing": "Le chemin n'existe pas - créez d'abord le répertoire contenant",
|
||||
"pathExists": "Le répertoire existe déjà - veuillez vous assurer que vous avez sauvegardé toutes les données",
|
||||
"pathValidationFailed": "Échec de la validation du chemin",
|
||||
"privacyPolicy": "politique de confidentialité",
|
||||
"selectItemsToMigrate": "Sélectionnez les éléments à migrer",
|
||||
"settings": {
|
||||
"allowMetrics": "Métriques d'utilisation",
|
||||
"allowMetricsDescription": "Aidez à améliorer ComfyUI en envoyant des métriques d'utilisation anonymes. Aucune information personnelle ou contenu de flux de travail ne sera collecté.",
|
||||
"autoUpdate": "Mises à jour automatiques",
|
||||
"autoUpdateDescription": "Téléchargez et installez automatiquement les mises à jour lorsqu'elles deviennent disponibles. Vous serez toujours informé avant l'installation des mises à jour.",
|
||||
"checkingMirrors": "Vérification de l'accès réseau aux miroirs python...",
|
||||
"dataCollectionDialog": {
|
||||
"collect": {
|
||||
"errorReports": "Message d'erreur et trace de la pile",
|
||||
"systemInfo": "Matériel, type de système d'exploitation et version de l'application",
|
||||
"userJourneyEvents": "Événements du parcours utilisateur"
|
||||
"userJourneyEvents": "Événements du parcours utilisateur",
|
||||
"userJourneyTooltip": "Les événements du parcours utilisateur sont utilisés pour suivre le parcours de l'utilisateur lors du processus d'installation de l'application. La collecte d'événements se termine lors de la première exécution réussie du flux de travail ComfyUI."
|
||||
},
|
||||
"doNotCollect": {
|
||||
"customNodeConfigurations": "Configurations de nœud personnalisées",
|
||||
@@ -234,54 +224,23 @@
|
||||
"workflowContents": "Contenus du flux de travail"
|
||||
},
|
||||
"title": "À propos de la collecte de données",
|
||||
"viewFullPolicy": "Voir la politique complète",
|
||||
"whatWeCollect": "Ce que nous collectons :",
|
||||
"whatWeDoNotCollect": "Ce que nous ne collectons pas :"
|
||||
},
|
||||
"errorUpdatingConsent": "Erreur de mise à jour du consentement",
|
||||
"errorUpdatingConsentDetail": "Échec de la mise à jour des paramètres de consentement aux métriques",
|
||||
"learnMoreAboutData": "En savoir plus sur la collecte de données",
|
||||
"mirrorSettings": "Paramètres du Miroir",
|
||||
"mirrorsReachable": "L'accès réseau aux miroirs python est bon",
|
||||
"mirrorsUnreachable": "L'accès au réseau à certains miroirs python est mauvais",
|
||||
"pypiMirrorPlaceholder": "Entrez l'URL du miroir PyPI",
|
||||
"pythonMirrorPlaceholder": "Entrez l'URL du miroir Python"
|
||||
"learnMoreAboutData": "En savoir plus sur la collecte de données"
|
||||
},
|
||||
"systemLocations": "Emplacements système",
|
||||
"unhandledError": "Erreur inconnue",
|
||||
"updateConsent": "Vous avez précédemment accepté de signaler les plantages. Nous suivons maintenant des métriques basées sur les événements pour aider à identifier les bugs et améliorer l'application. Aucune information personnelle identifiable n'est collectée."
|
||||
"unhandledError": "Erreur inconnue"
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "Contactez-moi pour un suivi",
|
||||
"feedbackTitle": "Aidez-nous à améliorer ComfyUI en fournissant des commentaires",
|
||||
"helpFix": "Aidez à résoudre cela",
|
||||
"notifyResolve": "Prévenez-moi lorsque résolu",
|
||||
"provideAdditionalDetails": "Fournir des détails supplémentaires (facultatif)",
|
||||
"provideEmail": "Donnez-nous votre email (Facultatif)",
|
||||
"rating": "Évaluation",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"maintenance": {
|
||||
"None": "Aucun",
|
||||
"OK": "OK",
|
||||
"Skipped": "Ignoré",
|
||||
"allOk": "Aucun problème détecté.",
|
||||
"confirmTitle": "Êtes-vous sûr ?",
|
||||
"detected": "Détecté",
|
||||
"error": {
|
||||
"defaultDescription": "Une erreur s'est produite lors de l'exécution d'une tâche de maintenance.",
|
||||
"taskFailed": "La tâche a échoué.",
|
||||
"toastTitle": "Erreur de tâche"
|
||||
},
|
||||
"refreshing": "Actualisation",
|
||||
"showManual": "Afficher les tâches de maintenance",
|
||||
"status": "Statut"
|
||||
"systemStats": "Statistiques du système"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "File d'attente automatique",
|
||||
@@ -319,7 +278,6 @@
|
||||
"Collapse/Expand Selected Nodes": "Réduire/Étendre les nœuds sélectionnés",
|
||||
"Comfy-Org Discord": "Discord de Comfy-Org",
|
||||
"ComfyUI Docs": "Docs de ComfyUI",
|
||||
"ComfyUI Forum": "Forum ComfyUI",
|
||||
"ComfyUI Issues": "Problèmes de ComfyUI",
|
||||
"Convert selected nodes to group node": "Convertir les nœuds sélectionnés en nœud de groupe",
|
||||
"Desktop User Guide": "Guide de l'utilisateur de bureau",
|
||||
@@ -327,9 +285,9 @@
|
||||
"Edit": "Éditer",
|
||||
"Export": "Exporter",
|
||||
"Export (API)": "Exporter (API)",
|
||||
"Feedback": "Retour d'information",
|
||||
"Fit Group To Contents": "Ajuster le groupe au contenu",
|
||||
"Fit view to selected nodes": "Ajuster la vue aux nœuds sélectionnés",
|
||||
"Give Feedback": "Donnez votre avis",
|
||||
"Group Selected Nodes": "Grouper les nœuds sélectionnés",
|
||||
"Help": "Aide",
|
||||
"Interrupt": "Interrompre",
|
||||
@@ -351,7 +309,6 @@
|
||||
"Previous Opened Workflow": "Flux de travail ouvert précédent",
|
||||
"Queue Prompt": "Invite de file d'attente",
|
||||
"Queue Prompt (Front)": "Invite de file d'attente (Front)",
|
||||
"Quit": "Quitter",
|
||||
"Redo": "Refaire",
|
||||
"Refresh Node Definitions": "Actualiser les définitions de nœud",
|
||||
"Reinstall": "Réinstaller",
|
||||
@@ -376,11 +333,6 @@
|
||||
"Zoom In": "Zoom avant",
|
||||
"Zoom Out": "Zoom arrière"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "Ne plus afficher ce message",
|
||||
"missingModels": "Modèles manquants",
|
||||
"missingModelsMessage": "Lors du chargement du graphique, les modèles suivants n'ont pas été trouvés"
|
||||
},
|
||||
"nodeCategories": {
|
||||
"3d": "3d",
|
||||
"3d_models": "modèles_3d",
|
||||
@@ -609,7 +561,6 @@
|
||||
"ColorPalette": "Palette de Couleurs",
|
||||
"Comfy": "Confort",
|
||||
"Comfy-Desktop": "Comfy-Desktop",
|
||||
"ContextMenu": "Menu Contextuel",
|
||||
"CustomColorPalettes": "Palettes de Couleurs Personnalisées",
|
||||
"DevMode": "Mode Développeur",
|
||||
"EditTokenWeight": "Modifier le Poids du Jeton",
|
||||
@@ -640,7 +591,6 @@
|
||||
"Settings": "Paramètres",
|
||||
"Sidebar": "Barre Latérale",
|
||||
"Tree Explorer": "Explorateur d'Arbre",
|
||||
"UV": "UV",
|
||||
"Validation": "Validation",
|
||||
"Window": "Fenêtre",
|
||||
"Workflow": "Flux de Travail"
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
}
|
||||
},
|
||||
"CLIPLoader": {
|
||||
"description": "[Recettes]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 / clip-g / clip-l\nstable_audio: t5\nmochi: t5\ncosmos: old t5 xxl",
|
||||
"description": "[Recettes]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 / clip-g / clip-l\nstable_audio: t5\nmochi: t5",
|
||||
"display_name": "Charger CLIP",
|
||||
"inputs": {
|
||||
"clip_name": {
|
||||
@@ -862,32 +862,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CosmosImageToVideoLatent": {
|
||||
"display_name": "CosmosImageVersVidéoLatent",
|
||||
"inputs": {
|
||||
"batch_size": {
|
||||
"name": "taille_du_lot"
|
||||
},
|
||||
"end_image": {
|
||||
"name": "image_de_fin"
|
||||
},
|
||||
"height": {
|
||||
"name": "hauteur"
|
||||
},
|
||||
"length": {
|
||||
"name": "longueur"
|
||||
},
|
||||
"start_image": {
|
||||
"name": "image_de_départ"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
},
|
||||
"width": {
|
||||
"name": "largeur"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateHookKeyframe": {
|
||||
"display_name": "Créer une image clé de crochet",
|
||||
"inputs": {
|
||||
@@ -1256,23 +1230,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmptyCosmosLatentVideo": {
|
||||
"display_name": "VidéoLatenteCosmosVide",
|
||||
"inputs": {
|
||||
"batch_size": {
|
||||
"name": "taille_du_lot"
|
||||
},
|
||||
"height": {
|
||||
"name": "hauteur"
|
||||
},
|
||||
"length": {
|
||||
"name": "longueur"
|
||||
},
|
||||
"width": {
|
||||
"name": "largeur"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmptyHunyuanLatentVideo": {
|
||||
"display_name": "EmptyHunyuanLatentVideo",
|
||||
"inputs": {
|
||||
@@ -4868,17 +4825,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SetFirstSigma": {
|
||||
"display_name": "DéfinirPremierSigma",
|
||||
"inputs": {
|
||||
"sigma": {
|
||||
"name": "sigma"
|
||||
},
|
||||
"sigmas": {
|
||||
"name": "sigmas"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SetHookKeyframes": {
|
||||
"display_name": "Définir les Images Clés de Crochet",
|
||||
"inputs": {
|
||||
|
||||
@@ -5,18 +5,6 @@
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "Envoyer des métriques d'utilisation anonymes"
|
||||
},
|
||||
"Comfy-Desktop_UV_PypiInstallMirror": {
|
||||
"name": "Miroir d'installation Pypi",
|
||||
"tooltip": "Miroir d'installation pip par défaut"
|
||||
},
|
||||
"Comfy-Desktop_UV_PythonInstallMirror": {
|
||||
"name": "Miroir d'installation Python",
|
||||
"tooltip": "Les installations Python gérées sont téléchargées depuis le projet Astral python-build-standalone. Cette variable peut être définie sur une URL de miroir pour utiliser une source différente pour les installations Python. L'URL fournie remplacera https://github.com/astral-sh/python-build-standalone/releases/download dans, par exemple, https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz. Les distributions peuvent être lues à partir d'un répertoire local en utilisant le schéma d'URL file://."
|
||||
},
|
||||
"Comfy-Desktop_UV_TorchInstallMirror": {
|
||||
"name": "Miroir d'installation Torch",
|
||||
"tooltip": "Miroir d'installation Pip pour pytorch"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "Style de fenêtre",
|
||||
"options": {
|
||||
@@ -321,9 +309,6 @@
|
||||
"name": "FPS maximum",
|
||||
"tooltip": "Le nombre maximum d'images par seconde que le canevas est autorisé à rendre. Limite l'utilisation du GPU au détriment de la fluidité. Si 0, le taux de rafraîchissement de l'écran est utilisé. Par défaut : 0"
|
||||
},
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "Mise à l'échelle des menus de widgets combinés de nœuds (listes) lors du zoom"
|
||||
},
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "Toujours aligner sur la grille"
|
||||
}
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "DevToolsを開く"
|
||||
},
|
||||
"Comfy-Desktop_OpenFeedbackPage": {
|
||||
"label": "フィードバック"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "デスクトップユーザーガイド"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "終了"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "再インストール"
|
||||
},
|
||||
@@ -83,9 +83,6 @@
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "ワークフローをエクスポート(API形式)"
|
||||
},
|
||||
"Comfy_Feedback": {
|
||||
"label": "フィードバック"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "グループを内容に合わせて調整"
|
||||
},
|
||||
@@ -110,9 +107,6 @@
|
||||
"Comfy_Help_OpenComfyUIDocs": {
|
||||
"label": "ComfyUIのドキュメントを開く"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIForum": {
|
||||
"label": "Comfy-Orgフォーラムを開く"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIIssues": {
|
||||
"label": "ComfyUIの問題を開く"
|
||||
},
|
||||
|
||||