Compare commits

..

10 Commits

Author SHA1 Message Date
Chenlei Hu
aeed923735 1.6.18 (#2177) 2025-01-06 16:11:32 -05:00
bymyself
ca33015ced Fix saved keybinding persistence (#2176) 2025-01-06 16:09:55 -05:00
Alexander Piskun
3195e8a697 (fix): added missing slash to "loadWorkflow" templates endpoint (#2174) 2025-01-06 16:09:41 -05:00
Chenlei Hu
66f3ccd7a8 Update litegraph 0.8.60 (#2164) 2025-01-06 16:09:23 -05:00
Chenlei Hu
31c46fe2b1 Prevent reference sharing on settingStore content (#2160) 2025-01-06 16:09:11 -05:00
Chenlei Hu
a16061d670 1.6.17 release (#2149)
Co-authored-by: filtered <176114999+webfiltered@users.noreply.github.com>
2025-01-03 16:10:09 -05:00
Chenlei Hu
77d45d8eff 1.6.16 (#2142) 2025-01-03 11:21:52 -05:00
Chenlei Hu
5972a07cfe 1.6 patches (#2141) 2025-01-03 11:20:25 -05:00
huchenlei
9d8633aafa Update release.yaml to target core/* branch 2025-01-03 11:17:16 -05:00
huchenlei
06292bccfc 1.6.15 2025-01-02 17:52:11 -05:00
164 changed files with 1796 additions and 6018 deletions

View File

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

View File

@@ -1,48 +0,0 @@
name: Update Node Definitions Locales
on:
workflow_dispatch:
inputs:
trigger_type:
description: 'Type of trigger (manual or automatic)'
required: false
type: string
default: 'manual'
jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: npm run dev:electron &
working-directory: ComfyUI_frontend
- name: Update en.json
run: npm run collect-i18n -- scripts/collect-i18n-node-defs.ts
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
working-directory: ComfyUI_frontend
- name: Update translations
run: npm run locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
working-directory: ComfyUI_frontend
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: "Update locales for node definitions"
title: "Update locales for node definitions"
body: |
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 }}
base: main
labels: dependencies
path: ComfyUI_frontend

View File

@@ -5,8 +5,6 @@ on:
jobs:
update-locales:
# Don't run on fork PRs
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
@@ -19,12 +17,13 @@ jobs:
run: npm run dev:electron &
working-directory: ComfyUI_frontend
- name: Update en.json
run: npm run collect-i18n -- scripts/collect-i18n-general.ts
run: npm run collect-i18n
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
working-directory: ComfyUI_frontend
- name: Update translations
run: npm run locale
# Pipe output so that it doesn't error out on stdout.clearLine
run: npm run locale 2>&1 | cat
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
working-directory: ComfyUI_frontend

View File

@@ -6,6 +6,7 @@ on:
branches:
- main
- master
- core/*
paths:
- "package.json"
@@ -25,8 +26,6 @@ jobs:
id: current_version
run: echo ::set-output name=version::$(node -p "require('./package.json').version")
- name: Build project
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
run: |
npm ci
npm run build

7
.gitignore vendored
View File

@@ -16,8 +16,6 @@ dist-ssr
.vscode/*
*.code-workspace
!.vscode/extensions.json
!.vscode/tailwind.json
!.vscode/settings.json.default
.idea
.DS_Store
*.suo
@@ -43,7 +41,4 @@ browser_tests/*/*-win32.png
dist.zip
/temp/
# Generated JSON Schemas
/schemas/
/temp/

View File

@@ -6,13 +6,5 @@
"printWidth": 80,
"importOrder": ["^@core/(.*)$", "<THIRD_PARTY_MODULES>", "^@/(.*)$", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"overrides": [
{
"files": "*.{js,cjs,mjs,ts,cts,mts,tsx,vue}",
"options": {
"plugins": ["@trivago/prettier-plugin-sort-imports"]
}
}
]
}
"importOrderSortSpecifiers": true
}

View File

@@ -1,5 +0,0 @@
{
"css.customData": [
".vscode/tailwind.json"
]
}

55
.vscode/tailwind.json vendored
View File

@@ -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 youd 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"
}
]
}
]
}

View File

@@ -58,7 +58,7 @@ Stable releases are published bi-weekly in the ComfyUI main repository.
### Major features
<details id='feature-native-translation'>
<details>
<summary>v1.5: Native translation (i18n)</summary>
ComfyUI now includes built-in translation support, replacing the need for third-party translation extensions. Select your language
@@ -68,7 +68,7 @@ Stable releases are published bi-weekly in the ComfyUI main repository.
More details available here: https://blog.comfy.org/p/native-localization-support-i18n
</details>
<details id='feature-mask-editor'>
<details>
<summary>v1.4: New mask editor</summary>
https://github.com/Comfy-Org/ComfyUI_frontend/pull/1284 implements a new mask editor.
@@ -76,7 +76,7 @@ Stable releases are published bi-weekly in the ComfyUI main repository.
![image](https://github.com/user-attachments/assets/f0ea6ee5-00ee-4e5d-a09c-6938e86a1f17)
</details>
<details id='feature-integrated-server-terminal'>
<details>
<summary>v1.3.22: Integrated server terminal</summary>
Press Ctrl + ` to toggle integrated terminal.
@@ -84,7 +84,7 @@ Press Ctrl + ` to toggle integrated terminal.
https://github.com/user-attachments/assets/eddedc6a-07a3-4a83-9475-63b3977f6d94
</details>
<details id='feature-keybinding-customization'>
<details>
<summary>v1.3.7: Keybinding customization</summary>
## Basic UI
@@ -101,7 +101,7 @@ https://github.com/user-attachments/assets/eddedc6a-07a3-4a83-9475-63b3977f6d94
</details>
<details id='feature-node-library-sidebar'>
<details>
<summary>v1.2.4: Node library sidebar tab</summary>
#### Drag & Drop
@@ -111,13 +111,13 @@ https://github.com/user-attachments/assets/853e20b7-bc0e-49c9-bbce-a2ba7566f92f
https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf
</details>
<details id='feature-queue-sidebar'>
<details>
<summary>v1.2.0: Queue/History sidebar tab</summary>
https://github.com/user-attachments/assets/86e264fe-4d26-4f07-aa9a-83bdd2d02b8f
</details>
<details id='feature-node-search'>
<details>
<summary>v1.1.0: Node search box</summary>
#### Fuzzy search & Node preview
@@ -129,26 +129,26 @@ https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf
### QoL changes
<details id='feature-nested-group'>
<details>
<summary>v1.3.32: **Litegraph** Nested group</summary>
https://github.com/user-attachments/assets/f51adeb1-028e-40af-81e4-0ac13075198a
</details>
<details id='feature-group-selection'>
<details>
<summary>v1.3.24: **Litegraph** Group selection</summary>
https://github.com/user-attachments/assets/e6230a94-411e-4fba-90cb-6c694200adaa
</details>
<details id='feature-toggle-link-visibility'>
<details>
<summary>v1.3.6: **Litegraph** Toggle link visibility</summary>
[rec.webm](https://github.com/user-attachments/assets/34e460ac-fbbc-44ef-bfbb-99a84c2ae2be)
</details>
<details id='feature-auto-widget-conversion'>
<details>
<summary>v1.3.4: **Litegraph** Auto widget to input conversion</summary>
Dropping a link of correct type on node widget will automatically convert the widget to input.
@@ -157,7 +157,7 @@ Dropping a link of correct type on node widget will automatically convert the wi
</details>
<details id='feature-pan-mode'>
<details>
<summary>v1.3.4: **Litegraph** Canvas pan mode</summary>
The canvas becomes readonly in pan mode. Pan mode is activated by clicking the pan mode button on the canvas menu
@@ -167,42 +167,42 @@ or by holding the space key.
</details>
<details id='feature-shift-drag-link-creation'>
<details>
<summary>v1.3.1: **Litegraph** Shift drag link to create a new link</summary>
[rec.webm](https://github.com/user-attachments/assets/7e73aaf9-79e2-4c3c-a26a-911cba3b85e4)
</details>
<details id='feature-optional-input-donuts'>
<details>
<summary>v1.2.62: **Litegraph** Show optional input slots as donuts</summary>
![GYEIRidb0AYGO-v](https://github.com/user-attachments/assets/e6cde0b6-654b-4afd-a117-133657a410b1)
</details>
<details id='feature-group-title-edit'>
<details>
<summary>v1.2.44: **Litegraph** Double click group title to edit</summary>
https://github.com/user-attachments/assets/5bf0e2b6-8b3a-40a7-b44f-f0879e9ad26f
</details>
<details id='feature-group-selection-shortcut'>
<details>
<summary>v1.2.39: **Litegraph** Group selected nodes with Ctrl + G</summary>
https://github.com/user-attachments/assets/7805dc54-0854-4a28-8bcd-4b007fa01151
</details>
<details id='feature-node-title-edit'>
<details>
<summary>v1.2.38: **Litegraph** Double click node title to edit</summary>
https://github.com/user-attachments/assets/d61d5d0e-f200-4153-b293-3e3f6a212b30
</details>
<details id='feature-drag-multi-link'>
<details>
<summary>v1.2.7: **Litegraph** drags multiple links with shift pressed</summary>
https://github.com/user-attachments/assets/68826715-bb55-4b2a-be6e-675cfc424afe
@@ -211,7 +211,7 @@ https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d
</details>
<details id='feature-auto-connect-link'>
<details>
<summary>v1.2.2: **Litegraph** auto connects to correct slot</summary>
#### Before
@@ -221,7 +221,7 @@ https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d
https://github.com/user-attachments/assets/b6360ac0-f0d2-447c-9daa-8a2e20c0dc1d
</details>
<details id='feature-hide-text-overflow'>
<details>
<summary>v1.1.8: **Litegraph** hides text overflow on widget value</summary>
https://github.com/user-attachments/assets/5696a89d-4a47-4fcc-9e8c-71e1264943f2
@@ -298,7 +298,7 @@ app.registerExtension({
</details>
<details id='extension-api-bottom-panel-tabs'>
<details>
<summary>v1.3.22: Register bottom panel tabs</summary>
```js
@@ -321,7 +321,7 @@ app.registerExtension({
</details>
<details id='extension-api-settings'>
<details>
<summary>v1.3.22: New settings API</summary>
Legacy settings API.
@@ -367,7 +367,7 @@ app.extensionManager.setting.set('TestSetting', 'Hello, universe!')
</details>
<details id='extension-api-commands-keybindings'>
<details>
<summary>v1.3.7: Register commands and keybindings</summary>
Extensions can call the following API to register commands and keybindings. Do
@@ -396,7 +396,7 @@ app.extensionManager.setting.set('TestSetting', 'Hello, universe!')
</details>
<details id='extension-api-topbar-menu'>
<details>
<summary>v1.3.1: Extension API to register custom topbar menu items</summary>
Extensions can call the following API to register custom topbar menu items.
@@ -425,7 +425,7 @@ app.extensionManager.setting.set('TestSetting', 'Hello, universe!')
![image](https://github.com/user-attachments/assets/ae7b082f-7ce9-4549-a446-4563567102fe)
</details>
<details id='extension-api-toast'>
<details>
<summary>v1.2.27: Extension API to add toast message</summary>i
Extensions can call the following API to add toast messages.
@@ -443,7 +443,7 @@ Documentation of all supported options can be found here: <https://primevue.org/
![image](https://github.com/user-attachments/assets/de02cd7e-cd81-43d1-a0b0-bccef92ff487)
</details>
<details id='extension-api-sidebar-tab'>
<details>
<summary>v1.2.4: Extension API to register custom sidebar tab</summary>
Extensions now can call the following API to register a sidebar tab.
@@ -626,3 +626,10 @@ You can switch languages by opening the ComfyUI Settings and selecting from the
- Option 1: Set `DEPLOY_COMFYUI_DIR` in `.env` and run `npm run deploy`.
- Option 2: Copy everything under `dist/` to `ComfyUI/web/` in your ComfyUI checkout manually.
## Publish release to ComfyUI main repo
Run following command to publish a release to ComfyUI main repo. The script will create a new branch and do a commit to `web/` folder by checkout `dist.zip`
from GitHub release.
- `python scripts/main_repo_release.py <path_to_comfyui_main_repo> <version>`

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,9 +1,8 @@
import { expect } from '@playwright/test'
import type { Palette } from '../src/types/colorPaletteTypes'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
const customColorPalettes: Record<string, Palette> = {
const customColorPalettes = {
obsidian: {
version: 102,
id: 'obsidian',
@@ -129,19 +128,6 @@ const customColorPalettes: Record<string, Palette> = {
'tr-odd-bg-color': 'rgba(19,19,19,.9)'
}
}
},
// A custom light theme with fg color red
light_red: {
id: 'light_red',
name: 'Light Red',
light_theme: true,
colors: {
node_slot: {},
litegraph_base: {},
comfy_base: {
'fg-color': '#ff0000'
}
}
}
}
@@ -150,18 +136,12 @@ test.describe('Color Palette', () => {
await comfyPage.setSetting('Comfy.CustomColorPalettes', customColorPalettes)
// Reload to apply the new setting. Setting Comfy.CustomColorPalettes directly
// doesn't update the store immediately.
await comfyPage.setup()
await comfyPage.reload()
await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark')
await expect(comfyPage.canvas).toHaveScreenshot(
'custom-color-palette-obsidian-dark.png'
)
await comfyPage.setSetting('Comfy.ColorPalette', 'light_red')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'custom-color-palette-light-red.png'
)
await comfyPage.setSetting('Comfy.ColorPalette', 'dark')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('default-color-palette.png')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

View File

@@ -11,14 +11,6 @@ test.describe('Copy Paste', () => {
await expect(comfyPage.canvas).toHaveScreenshot('copied-node.png')
})
test('Can copy and paste node with link', async ({ comfyPage }) => {
await comfyPage.clickTextEncodeNode1()
await comfyPage.page.mouse.move(10, 10)
await comfyPage.ctrlC()
await comfyPage.page.keyboard.press('Control+Shift+V')
await expect(comfyPage.canvas).toHaveScreenshot('copied-node-with-link.png')
})
test('Can copy and paste text', async ({ comfyPage }) => {
const textBox = comfyPage.widgetTextBox
await textBox.click()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

View File

@@ -44,18 +44,6 @@ test.describe('Execution error', () => {
const executionError = comfyPage.page.locator('.comfy-error-report')
await expect(executionError).toBeVisible()
})
test('Can display Issue Report form', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('execution_error')
await comfyPage.queueButton.click()
await comfyPage.nextFrame()
await comfyPage.page.getByLabel('Help Fix This').click()
const issueReportForm = comfyPage.page.getByText(
'Submit Error Report (Optional)'
)
await expect(issueReportForm).toBeVisible()
})
})
test.describe('Missing models warning', () => {
@@ -165,37 +153,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()
})
})

View File

@@ -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()
})
})

View File

@@ -27,11 +27,6 @@ dotenv.config()
type WorkspaceStore = ReturnType<typeof useWorkspaceStore>
class ComfyMenu {
private _nodeLibraryTab: NodeLibrarySidebarTab | null = null
private _workflowsTab: WorkflowsSidebarTab | null = null
private _queueTab: QueueSidebarTab | null = null
private _topbar: Topbar | null = null
public readonly sideToolbar: Locator
public readonly themeToggleButton: Locator
public readonly saveButton: Locator
@@ -45,23 +40,19 @@ class ComfyMenu {
}
get nodeLibraryTab() {
this._nodeLibraryTab ??= new NodeLibrarySidebarTab(this.page)
return this._nodeLibraryTab
return new NodeLibrarySidebarTab(this.page)
}
get workflowsTab() {
this._workflowsTab ??= new WorkflowsSidebarTab(this.page)
return this._workflowsTab
return new WorkflowsSidebarTab(this.page)
}
get queueTab() {
this._queueTab ??= new QueueSidebarTab(this.page)
return this._queueTab
return new QueueSidebarTab(this.page)
}
get topbar() {
this._topbar ??= new Topbar(this.page)
return this._topbar
return new Topbar(this.page)
}
async toggleTheme() {
@@ -369,6 +360,11 @@ export class ComfyPage {
}, settingId)
}
async reload({ clearStorage = true }: { clearStorage?: boolean } = {}) {
await this.page.reload({ timeout: 15000 })
await this.setup({ clearStorage })
}
async goto() {
await this.page.goto(this.url)
}

View File

@@ -3,13 +3,6 @@ import { Locator, Page } from '@playwright/test'
export class ComfyNodeSearchFilterSelectionPanel {
constructor(public readonly page: Page) {}
get header() {
return this.page
.getByRole('dialog')
.locator('div')
.filter({ hasText: 'Add node filter condition' })
}
async selectFilterType(filterType: string) {
await this.page
.locator(

View File

@@ -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')
}
}

View File

@@ -9,12 +9,6 @@ export class Topbar {
.allInnerTexts()
}
async getActiveTabName(): Promise<string> {
return this.page
.locator('.workflow-tabs .p-togglebutton-checked')
.innerText()
}
async openSubmenuMobile() {
await this.page.locator('.p-menubar-mobile .p-menubar-button').click()
}

View File

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

View File

@@ -593,7 +593,7 @@ test.describe('Load workflow', () => {
}) => {
await comfyPage.loadWorkflow('single_ksampler')
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
await comfyPage.setup({ clearStorage: false })
await comfyPage.reload({ clearStorage: false })
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
})
@@ -610,72 +610,11 @@ test.describe('Load workflow', () => {
await expect(comfyPage.canvas).toHaveScreenshot(
'single_ksampler_modified.png'
)
await comfyPage.setup({ clearStorage: false })
await comfyPage.reload({ clearStorage: false })
await expect(comfyPage.canvas).toHaveScreenshot(
'single_ksampler_modified.png'
)
})
test.describe('Restore all open workflows on reload', () => {
let workflowA: string
let workflowB: string
const generateUniqueFilename = (extension = '') =>
`${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}${extension}`
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
workflowA = generateUniqueFilename()
await comfyPage.menu.topbar.saveWorkflow(workflowA)
workflowB = generateUniqueFilename()
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
await comfyPage.menu.topbar.saveWorkflow(workflowB)
// Wait for localStorage to persist the workflow paths before reloading
await comfyPage.page.waitForFunction(
() => !!window.localStorage.getItem('Comfy.OpenWorkflowsPaths')
)
await comfyPage.setup({ clearStorage: false })
})
test('Restores topbar workflow tabs after reload', async ({
comfyPage
}) => {
await comfyPage.setSetting(
'Comfy.Workflow.WorkflowTabsPosition',
'Topbar'
)
const tabs = await comfyPage.menu.topbar.getTabNames()
const activeWorkflowName = await comfyPage.menu.topbar.getActiveTabName()
expect(tabs).toEqual(expect.arrayContaining([workflowA, workflowB]))
expect(tabs.indexOf(workflowA)).toBeLessThan(tabs.indexOf(workflowB))
expect(activeWorkflowName).toEqual(workflowB)
})
test('Restores sidebar workflows after reload', async ({ comfyPage }) => {
await comfyPage.setSetting(
'Comfy.Workflow.WorkflowTabsPosition',
'Sidebar'
)
await comfyPage.menu.workflowsTab.open()
const openWorkflows =
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
const activeWorkflowName =
await comfyPage.menu.workflowsTab.getActiveWorkflowName()
const workflowPathA = `${workflowA}.json`
const workflowPathB = `${workflowB}.json`
expect(openWorkflows).toEqual(
expect.arrayContaining([workflowPathA, workflowPathB])
)
expect(openWorkflows.indexOf(workflowPathA)).toBeLessThan(
openWorkflows.indexOf(workflowPathB)
)
expect(activeWorkflowName).toEqual(workflowPathB)
})
})
})
test.describe('Load duplicate workflow', () => {

View File

@@ -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 }) => {
@@ -18,7 +21,7 @@ test.describe('Menu', () => {
expect(await comfyPage.menu.getThemeId()).toBe('light')
// Theme id should persist after reload.
await comfyPage.setup()
await comfyPage.reload()
expect(await comfyPage.menu.getThemeId()).toBe('light')
await comfyPage.menu.toggleTheme()
@@ -289,47 +292,6 @@ test.describe('Menu', () => {
}
})
})
test('Can customize bookmark color after interacting with color options', async ({
comfyPage
}) => {
// Open customization dialog
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
const tab = comfyPage.menu.nodeLibraryTab
await tab.getFolder('foo').click({ button: 'right' })
await comfyPage.page.getByLabel('Customize').click()
// Click a color option multiple times
const customColorOption = comfyPage.page.locator(
'.p-togglebutton-content > .pi-palette'
)
await customColorOption.click()
await customColorOption.click()
// Use the color picker
await comfyPage.page
.getByLabel('Customize Folder')
.getByRole('textbox')
.click()
await comfyPage.page.locator('.p-colorpicker-color-background').click()
// Finalize the customization
await comfyPage.page
.locator('.icon-field .p-selectbutton > *:nth-child(2)')
.click()
await comfyPage.page.getByLabel('Confirm').click()
await comfyPage.nextFrame()
// Verify the color selection is saved
const setting = await comfyPage.getSetting(
'Comfy.NodeLibrary.BookmarksCustomization'
)
await expect(setting).toHaveProperty(['foo/', 'color'])
await expect(setting['foo/'].color).not.toBeNull()
await expect(setting['foo/'].color).not.toBeUndefined()
await expect(setting['foo/'].color).not.toBe('')
})
test('Can rename customized bookmark folder', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
await comfyPage.setSetting('Comfy.NodeLibrary.BookmarksCustomization', {
@@ -566,7 +528,7 @@ test.describe('Menu', () => {
})
await comfyPage.setSetting('Comfy.Locale', 'zh')
await comfyPage.setup()
await comfyPage.reload()
const downloadedContentZh = await comfyPage.getExportedWorkflow({
api: false
@@ -662,15 +624,6 @@ test.describe('Menu', () => {
).toEqual(['*Unsaved Workflow.json'])
})
test('Can close saved workflow with command', async ({ comfyPage }) => {
const tab = comfyPage.menu.workflowsTab
await comfyPage.menu.topbar.saveWorkflow('workflow1.json')
await comfyPage.executeCommand('Workspace.CloseWorkflow')
expect(await tab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json'
])
})
test('Can delete workflows (confirm disabled)', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Workflow.ConfirmDelete', false)
@@ -805,7 +758,7 @@ test.describe('Menu', () => {
})
})
test.describe.skip('Queue sidebar', () => {
test.describe('Queue sidebar', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})
@@ -945,61 +898,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()
})
})
})

View File

@@ -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)
})
@@ -136,48 +132,6 @@ test.describe('Node search box', () => {
await expectFilterChips(comfyPage, ['MODEL'])
})
// Flaky test.
// Sample test failure:
// https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/12696912248/job/35391990861?pr=2210
/*
1) [chromium-2x] nodeSearchBox.spec.ts:135:5 Node search box Filtering Outer click dismisses filter panel but keeps search box visible
Error: expect(locator).not.toBeVisible()
Locator: getByRole('dialog').locator('div').filter({ hasText: 'Add node filter condition' })
Expected: not visible
Received: visible
Call log:
- expect.not.toBeVisible with timeout 5000ms
- waiting for getByRole('dialog').locator('div').filter({ hasText: 'Add node filter condition' })
143 |
144 | // Verify the filter selection panel is hidden
> 145 | expect(panel.header).not.toBeVisible()
| ^
146 |
147 | // Verify the node search dialog is still visible
148 | expect(comfyPage.searchBox.input).toBeVisible()
at /home/runner/work/ComfyUI_frontend/ComfyUI_frontend/ComfyUI_frontend/browser_tests/nodeSearchBox.spec.ts:145:32
*/
test.skip('Outer click dismisses filter panel but keeps search box visible', async ({
comfyPage
}) => {
await comfyPage.searchBox.filterButton.click()
const panel = comfyPage.searchBox.filterSelectionPanel
await panel.header.waitFor({ state: 'visible' })
const panelBounds = await panel.header.boundingBox()
await comfyPage.page.mouse.click(panelBounds!.x - 10, panelBounds!.y - 10)
// Verify the filter selection panel is hidden
expect(panel.header).not.toBeVisible()
// Verify the node search dialog is still visible
expect(comfyPage.searchBox.input).toBeVisible()
})
test('Can add multiple filters', async ({ comfyPage }) => {
await comfyPage.searchBox.addFilter('MODEL', 'Input Type')
await comfyPage.searchBox.addFilter('CLIP', 'Output Type')

2
global.d.ts vendored
View File

@@ -1,3 +1 @@
declare const __COMFYUI_FRONTEND_VERSION__: string
declare const __SENTRY_ENABLED__: boolean
declare const __SENTRY_DSN__: string

View File

@@ -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(' ')}`
]
}

444
package-lock.json generated
View File

@@ -1,20 +1,18 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.8.2",
"version": "1.6.18",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@comfyorg/comfyui-frontend",
"version": "1.8.2",
"version": "1.6.18",
"license": "GPL-3.0-only",
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.11",
"@comfyorg/litegraph": "^0.8.61",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",
"@comfyorg/comfyui-electron-types": "^0.4.3",
"@comfyorg/litegraph": "^0.8.60",
"@primevue/themes": "^4.0.5",
"@tiptap/core": "^2.10.4",
"@tiptap/extension-link": "^2.10.4",
"@tiptap/extension-table": "^2.10.4",
@@ -33,10 +31,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",
@@ -89,8 +87,7 @@
"vite-plugin-static-copy": "^1.0.5",
"vitest": "^2.0.5",
"vue-tsc": "^2.1.10",
"zip-dir": "^2.0.0",
"zod-to-json-schema": "^3.24.1"
"zip-dir": "^2.0.0"
}
},
"../litegraph.js": {
@@ -1937,15 +1934,15 @@
"dev": true
},
"node_modules/@comfyorg/comfyui-electron-types": {
"version": "0.4.11",
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.11.tgz",
"integrity": "sha512-RGJeWwXjyv0Ojj7xkZKgcRxC1nFv1nh7qEWpNBiofxVgFiap9Ei79b/KJYxNE0no4BoYqRMaRg+sFtCE6yEukA==",
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.3.tgz",
"integrity": "sha512-hSM3mchpsYN0e7oZ7XLWjEvFDvE1rgzaB9YkCeqIiZYZgLL78T79ssM0n5ra17Zv7Mqwl6ErZblXvbQE/36RPw==",
"license": "GPL-3.0-only"
},
"node_modules/@comfyorg/litegraph": {
"version": "0.8.61",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.61.tgz",
"integrity": "sha512-7DroJ0PLgI9TFvQR//6rf0NRXRvV60hapxVX5lmKzNn4Mn2Ni/JsB2ypNLKeSU5sacNyu8QT3W5Jdpafl7lcnA==",
"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 +3959,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"
@@ -4530,96 +4461,6 @@
"string-argv": "~0.3.1"
}
},
"node_modules/@sentry-internal/browser-utils": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.48.0.tgz",
"integrity": "sha512-pLtu0Fa1Ou0v3M1OEO1MB1EONJVmXEGtoTwFRCO1RPQI2ulmkG6BikINClFG5IBpoYKZ33WkEXuM6U5xh+pdZg==",
"dependencies": {
"@sentry/core": "8.48.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry-internal/feedback": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.48.0.tgz",
"integrity": "sha512-6PwcJNHVPg0EfZxmN+XxVOClfQpv7MBAweV8t9i5l7VFr8sM/7wPNSeU/cG7iK19Ug9ZEkBpzMOe3G4GXJ5bpw==",
"dependencies": {
"@sentry/core": "8.48.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry-internal/replay": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.48.0.tgz",
"integrity": "sha512-csILVupc5RkrsTrncuUTGmlB56FQSFjXPYWG8I8yBTGlXEJ+o8oTuF6+55R4vbw3EIzBveXWi4kEBbnQlXW/eg==",
"dependencies": {
"@sentry-internal/browser-utils": "8.48.0",
"@sentry/core": "8.48.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry-internal/replay-canvas": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.48.0.tgz",
"integrity": "sha512-LdivLfBXXB9us1aAc6XaL7/L2Ob4vi3C/fEOXElehg3qHjX6q6pewiv5wBvVXGX1NfZTRvu+X11k6TZoxKsezw==",
"dependencies": {
"@sentry-internal/replay": "8.48.0",
"@sentry/core": "8.48.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry/browser": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.48.0.tgz",
"integrity": "sha512-fuuVULB5/1vI8NoIwXwR3xwhJJqk+y4RdSdajExGF7nnUDBpwUJyXsmYJnOkBO+oLeEs58xaCpotCKiPUNnE3g==",
"dependencies": {
"@sentry-internal/browser-utils": "8.48.0",
"@sentry-internal/feedback": "8.48.0",
"@sentry-internal/replay": "8.48.0",
"@sentry-internal/replay-canvas": "8.48.0",
"@sentry/core": "8.48.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry/core": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.48.0.tgz",
"integrity": "sha512-VGwYgTfLpvJ5LRO5A+qWo1gpo6SfqaGXL9TOzVgBucAdpzbrYHpZ87sEarDVq/4275uk1b0S293/mfsskFczyw==",
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry/vue": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@sentry/vue/-/vue-8.48.0.tgz",
"integrity": "sha512-hqm9X7hz1vMQQB1HBYezrDBQihZk6e/MxWIG1wMJoClcBnD1Sh7y+D36UwaQlR4Gr/Ftiz+Bb0DxuAYHoUS4ow==",
"dependencies": {
"@sentry/browser": "8.48.0",
"@sentry/core": "8.48.0"
},
"engines": {
"node": ">=14.18"
},
"peerDependencies": {
"pinia": "2.x",
"vue": "2.x || 3.x"
},
"peerDependenciesMeta": {
"pinia": {
"optional": true
}
}
},
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -5793,53 +5634,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 +5720,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 +5779,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 +7755,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 +14335,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 +15078,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 +15244,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 +15261,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 +15436,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 +18724,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": "*"
@@ -19581,22 +19440,21 @@
}
},
"node_modules/zod": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
"license": "MIT",
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-to-json-schema": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz",
"integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==",
"version": "3.23.5",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.23.5.tgz",
"integrity": "sha512-5wlSS0bXfF/BrL4jPAbz9da5hDlDptdEppYfe+x4eIJ7jioqKG9uUxOwPzqof09u/XeVdrgFu29lZi+8XNDJtA==",
"dev": true,
"license": "ISC",
"peerDependencies": {
"zod": "^3.24.1"
"zod": "^3.23.3"
}
},
"node_modules/zod-validation-error": {

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.8.2",
"version": "1.6.18",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -17,8 +17,8 @@
"update-litegraph": "node scripts/update-litegraph.js",
"zipdist": "node scripts/zipdist.js",
"typecheck": "vue-tsc --noEmit && tsc --noEmit && tsc-strict",
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}'",
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --plugin @trivago/prettier-plugin-sort-imports",
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}' --plugin @trivago/prettier-plugin-sort-imports",
"test:jest": "jest --config jest.config.ts",
"test:generate": "npx tsx tests-ui/setup",
"test:browser": "npx playwright test",
@@ -28,8 +28,7 @@
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"locale": "lobe-i18n locale",
"collect-i18n": "playwright test --config=playwright.i18n.config.ts",
"json-schema": "tsx scripts/generate-json-schema.ts"
"collect-i18n": "playwright test --config=playwright.i18n.config.ts"
},
"devDependencies": {
"@babel/core": "^7.24.7",
@@ -78,16 +77,13 @@
"vite-plugin-static-copy": "^1.0.5",
"vitest": "^2.0.5",
"vue-tsc": "^2.1.10",
"zip-dir": "^2.0.0",
"zod-to-json-schema": "^3.24.1"
"zip-dir": "^2.0.0"
},
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.11",
"@comfyorg/litegraph": "^0.8.61",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",
"@comfyorg/comfyui-electron-types": "^0.4.3",
"@comfyorg/litegraph": "^0.8.60",
"@primevue/themes": "^4.0.5",
"@tiptap/core": "^2.10.4",
"@tiptap/extension-link": "^2.10.4",
"@tiptap/extension-table": "^2.10.4",
@@ -106,10 +102,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",

View File

@@ -8,7 +8,7 @@ const config: PlaywrightTestConfig = {
},
reporter: 'list',
timeout: 10000,
testMatch: /collect-i18n-.*\.ts/
testMatch: /collect-i18n\.ts/
}
export default config

View File

@@ -1,125 +0,0 @@
import * as fs from 'fs'
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
import type { ComfyApi } from '../src/scripts/api'
import { ComfyNodeDefImpl } from '../src/stores/nodeDefStore'
import { normalizeI18nKey } from '../src/utils/formatUtil'
const localePath = './src/locales/en/main.json'
const nodeDefsPath = './src/locales/en/nodeDefs.json'
test('collect-i18n-node-defs', async ({ comfyPage }) => {
const nodeDefs: ComfyNodeDefImpl[] = Object.values(
await comfyPage.page.evaluate(async () => {
const api = window['app'].api as ComfyApi
return await api.getNodeDefs()
})
).map((def) => new ComfyNodeDefImpl(def))
console.log(`Collected ${nodeDefs.length} node definitions`)
const allDataTypesLocale = Object.fromEntries(
nodeDefs
.flatMap((nodeDef) => {
const inputDataTypes = Object.values(nodeDef.inputs.all).map(
(inputSpec) => inputSpec.type
)
const outputDataTypes = nodeDef.outputs.all.map((output) => output.type)
const allDataTypes = [...inputDataTypes, ...outputDataTypes].flatMap(
(type: string) => type.split(',')
)
return allDataTypes.map((dataType) => [
normalizeI18nKey(dataType),
dataType
])
})
.sort((a, b) => a[0].localeCompare(b[0]))
)
function extractInputs(nodeDef: ComfyNodeDefImpl) {
const inputs = Object.fromEntries(
nodeDef.inputs.all.flatMap((input) => {
const name = input.name
const tooltip = input.tooltip
if (name === undefined && tooltip === undefined) {
return []
}
return [
[
normalizeI18nKey(input.name),
{
name,
tooltip
}
]
]
})
)
return Object.keys(inputs).length > 0 ? inputs : undefined
}
function extractOutputs(nodeDef: ComfyNodeDefImpl) {
const outputs = Object.fromEntries(
nodeDef.outputs.all.flatMap((output, i) => {
// Ignore data types if they are already translated in allDataTypesLocale.
const name = output.name in allDataTypesLocale ? undefined : output.name
const tooltip = output.tooltip
if (name === undefined && tooltip === undefined) {
return []
}
return [
[
i.toString(),
{
name,
tooltip
}
]
]
})
)
return Object.keys(outputs).length > 0 ? outputs : undefined
}
const allNodeDefsLocale = Object.fromEntries(
nodeDefs
.sort((a, b) => a.name.localeCompare(b.name))
.map((nodeDef) => [
normalizeI18nKey(nodeDef.name),
{
display_name: nodeDef.display_name ?? nodeDef.name,
description: nodeDef.description || undefined,
inputs: extractInputs(nodeDef),
outputs: extractOutputs(nodeDef)
}
])
)
const allNodeCategoriesLocale = Object.fromEntries(
nodeDefs.flatMap((nodeDef) =>
nodeDef.category
.split('/')
.map((category) => [normalizeI18nKey(category), category])
)
)
const locale = JSON.parse(fs.readFileSync(localePath, 'utf-8'))
fs.writeFileSync(
localePath,
JSON.stringify(
{
...locale,
dataTypes: allDataTypesLocale,
nodeCategories: allNodeCategoriesLocale
},
null,
2
)
)
fs.writeFileSync(nodeDefsPath, JSON.stringify(allNodeDefsLocale, null, 2))
})

View File

@@ -3,11 +3,14 @@ import * as fs from 'fs'
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
import { CORE_MENU_COMMANDS } from '../src/constants/coreMenuCommands'
import { SERVER_CONFIG_ITEMS } from '../src/constants/serverConfig'
import type { ComfyApi } from '../src/scripts/api'
import type { ComfyCommandImpl } from '../src/stores/commandStore'
import { ComfyNodeDefImpl } from '../src/stores/nodeDefStore'
import type { FormItem, SettingParams } from '../src/types/settingTypes'
import { formatCamelCase, normalizeI18nKey } from '../src/utils/formatUtil'
const localePath = './src/locales/en/main.json'
const nodeDefsPath = './src/locales/en/nodeDefs.json'
const commandsPath = './src/locales/en/commands.json'
const settingsPath = './src/locales/en/settings.json'
@@ -19,7 +22,7 @@ const extractMenuCommandLocaleStrings = (): Set<string> => {
return labels
}
test('collect-i18n-general', async ({ comfyPage }) => {
test('collect-i18n', async ({ comfyPage }) => {
const commands = (
await comfyPage.page.evaluate(() => {
const workspace = window['app'].extensionManager
@@ -126,6 +129,105 @@ test('collect-i18n-general', async ({ comfyPage }) => {
])
)
// Node Definitions
const nodeDefs: ComfyNodeDefImpl[] = Object.values(
await comfyPage.page.evaluate(async () => {
const api = window['app'].api as ComfyApi
return await api.getNodeDefs()
})
).map((def) => new ComfyNodeDefImpl(def))
console.log(`Collected ${nodeDefs.length} node definitions`)
const allDataTypesLocale = Object.fromEntries(
nodeDefs
.flatMap((nodeDef) => {
const inputDataTypes = Object.values(nodeDef.inputs.all).map(
(inputSpec) => inputSpec.type
)
const outputDataTypes = nodeDef.outputs.all.map((output) => output.type)
const allDataTypes = [...inputDataTypes, ...outputDataTypes].flatMap(
(type: string) => type.split(',')
)
return allDataTypes.map((dataType) => [
normalizeI18nKey(dataType),
dataType
])
})
.sort((a, b) => a[0].localeCompare(b[0]))
)
function extractInputs(nodeDef: ComfyNodeDefImpl) {
const inputs = Object.fromEntries(
nodeDef.inputs.all.flatMap((input) => {
const name = input.name
const tooltip = input.tooltip
if (name === undefined && tooltip === undefined) {
return []
}
return [
[
normalizeI18nKey(input.name),
{
name,
tooltip
}
]
]
})
)
return Object.keys(inputs).length > 0 ? inputs : undefined
}
function extractOutputs(nodeDef: ComfyNodeDefImpl) {
const outputs = Object.fromEntries(
nodeDef.outputs.all.flatMap((output, i) => {
// Ignore data types if they are already translated in allDataTypesLocale.
const name = output.name in allDataTypesLocale ? undefined : output.name
const tooltip = output.tooltip
if (name === undefined && tooltip === undefined) {
return []
}
return [
[
i.toString(),
{
name,
tooltip
}
]
]
})
)
return Object.keys(outputs).length > 0 ? outputs : undefined
}
const allNodeDefsLocale = Object.fromEntries(
nodeDefs
.sort((a, b) => a.name.localeCompare(b.name))
.map((nodeDef) => [
normalizeI18nKey(nodeDef.name),
{
display_name: nodeDef.display_name ?? nodeDef.name,
description: nodeDef.description || undefined,
inputs: extractInputs(nodeDef),
outputs: extractOutputs(nodeDef)
}
])
)
const allNodeCategoriesLocale = Object.fromEntries(
nodeDefs.flatMap((nodeDef) =>
nodeDef.category
.split('/')
.map((category) => [normalizeI18nKey(category), category])
)
)
fs.writeFileSync(
localePath,
JSON.stringify(
@@ -139,13 +241,16 @@ test('collect-i18n-general', async ({ comfyPage }) => {
...allSettingCategoriesLocale
},
serverConfigItems: allServerConfigsLocale,
serverConfigCategories: allServerConfigCategoriesLocale
serverConfigCategories: allServerConfigCategoriesLocale,
dataTypes: allDataTypesLocale,
nodeCategories: allNodeCategoriesLocale
},
null,
2
)
)
fs.writeFileSync(nodeDefsPath, JSON.stringify(allNodeDefsLocale, null, 2))
fs.writeFileSync(commandsPath, JSON.stringify(allCommandsLocale, null, 2))
fs.writeFileSync(settingsPath, JSON.stringify(allSettingsLocale, null, 2))
})

View File

@@ -1,35 +0,0 @@
import fs from 'fs'
import path from 'path'
import { zodToJsonSchema } from 'zod-to-json-schema'
import { zComfyWorkflow, zComfyWorkflow1 } from '../src/types/comfyWorkflow'
// Convert both workflow schemas to JSON Schema
const workflow04Schema = zodToJsonSchema(zComfyWorkflow, {
name: 'ComfyWorkflow0_4',
$refStrategy: 'none'
})
const workflow1Schema = zodToJsonSchema(zComfyWorkflow1, {
name: 'ComfyWorkflow1_0',
$refStrategy: 'none'
})
// Create output directory if it doesn't exist
const outputDir = './schemas'
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true })
}
// Write schemas to files
fs.writeFileSync(
path.join(outputDir, 'workflow-0_4.json'),
JSON.stringify(workflow04Schema, null, 2)
)
fs.writeFileSync(
path.join(outputDir, 'workflow-1_0.json'),
JSON.stringify(workflow1Schema, null, 2)
)
console.log('JSON Schemas generated successfully!')

View File

@@ -0,0 +1,71 @@
import os
import sys
import requests
import zipfile
import shutil
import git
import tempfile
def download_and_extract(version, temp_dir):
url = f"https://github.com/Comfy-Org/ComfyUI_frontend/releases/download/v{version}/dist.zip"
response = requests.get(url)
if response.status_code == 200:
zip_path = os.path.join(temp_dir, "dist.zip")
with open(zip_path, "wb") as file:
file.write(response.content)
with zipfile.ZipFile(zip_path, "r") as zip_ref:
zip_ref.extractall(temp_dir)
# Clean up the zip file after extraction
os.remove(zip_path)
else:
raise Exception(
f"Failed to download release asset. Status code: {response.status_code}"
)
def update_repo(repo_path, version, temp_dir):
repo = git.Repo(repo_path)
# Stash any changes
repo.git.stash()
# Create and checkout new branch
new_branch = f"release-{version}"
repo.git.checkout("-b", new_branch, "-t", "origin/master")
# Remove all files under web/ directory
web_dir = os.path.join(repo_path, "web")
if os.path.exists(web_dir):
shutil.rmtree(web_dir)
# Move content from temp_dir to web/
shutil.move(temp_dir, web_dir)
# Add changes and commit
repo.git.add(all=True)
commit_message = f"Update web content to release v{version}"
repo.git.commit("-m", commit_message)
def main(repo_path: str, version: str):
with tempfile.TemporaryDirectory() as temp_dir:
try:
download_and_extract(version, temp_dir)
update_repo(repo_path, version, temp_dir)
print(f"Successfully updated repo to release v{version}")
except Exception as e:
print(f"An error occurred: {str(e)}")
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python script.py <repo_path> <version>")
sys.exit(1)
repo_path = sys.argv[1]
version = sys.argv[2]
main(repo_path, version)

View File

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

View File

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

View File

@@ -10,7 +10,6 @@
--bg-color: #fff;
--comfy-menu-bg: #353535;
--comfy-menu-secondary-bg: #292929;
--comfy-topbar-height: 2.5rem;
--comfy-input-bg: #222;
--input-text: #ddd;
--descrip-text: #999;
@@ -764,17 +763,3 @@ audio.comfy-audio.empty-audio-widget {
.p-tree-node-content {
padding: var(--comfy-tree-explorer-item-padding) !important;
}
/* [Desktop] Electron window specific styles */
.app-drag {
app-region: drag;
}
.no-drag {
app-region: no-drag;
}
.window-actions-spacer {
width: calc(100vw - env(titlebar-area-width, 100vw));
}
/* End of [Desktop] Electron window specific styles */

View File

@@ -27,7 +27,6 @@
"NODE_SELECTED_TITLE_COLOR": "#FFF",
"NODE_TEXT_SIZE": 14,
"NODE_TEXT_COLOR": "#AAA",
"NODE_TEXT_HIGHLIGHT_COLOR": "#FFF",
"NODE_SUBTEXT_SIZE": 12,
"NODE_DEFAULT_COLOR": "#333",
"NODE_DEFAULT_BGCOLOR": "#353535",

View File

@@ -1,7 +1,6 @@
{
"id": "light",
"name": "Light",
"light_theme": true,
"colors": {
"node_slot": {
"CLIP": "#FFA726",
@@ -14,12 +13,7 @@
"MASK": "#9CCC65",
"MODEL": "#7E57C2",
"STYLE_MODEL": "#D4E157",
"VAE": "#FF7043",
"NOISE": "#B0B0B0",
"GUIDER": "#66FFFF",
"SAMPLER": "#ECB4B4",
"SIGMAS": "#CDFFCD",
"TAESD": "#DCC274"
"VAE": "#FF7043"
},
"litegraph_base": {
"BACKGROUND_IMAGE": "data:image/gif;base64,R0lGODlhZABkALMAAAAAAP///+vr6+rq6ujo6Ofn5+bm5uXl5d3d3f///wAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAkALAAAAABkAGQAAAT/UMhJq7046827HkcoHkYxjgZhnGG6si5LqnIM0/fL4qwwIMAg0CAsEovBIxKhRDaNy2GUOX0KfVFrssrNdpdaqTeKBX+dZ+jYvEaTf+y4W66mC8PUdrE879f9d2mBeoNLfH+IhYBbhIx2jkiHiomQlGKPl4uZe3CaeZifnnijgkESBqipqqusra6vsLGys62SlZO4t7qbuby7CLa+wqGWxL3Gv3jByMOkjc2lw8vOoNSi0czAncXW3Njdx9Pf48/Z4Kbbx+fQ5evZ4u3k1fKR6cn03vHlp7T9/v8A/8Gbp4+gwXoFryXMB2qgwoMMHyKEqA5fxX322FG8tzBcRnMW/zlulPbRncmQGidKjMjyYsOSKEF2FBlJQMCbOHP6c9iSZs+UnGYCdbnSo1CZI5F64kn0p1KnTH02nSoV3dGTV7FFHVqVq1dtWcMmVQZTbNGu72zqXMuW7danVL+6e4t1bEy6MeueBYLXrNO5Ze36jQtWsOG97wIj1vt3St/DjTEORss4nNq2mDP3e7w4r1bFkSET5hy6s2TRlD2/mSxXtSHQhCunXo26NevCpmvD/UU6tuullzULH76q92zdZG/Ltv1a+W+osI/nRmyc+fRi1Xdbh+68+0vv10dH3+77KD/i6IdnX669/frn5Zsjh4/2PXju8+8bzc9/6fj27LFnX11/+IUnXWl7BJfegm79FyB9JOl3oHgSklefgxAC+FmFGpqHIYcCfkhgfCohSKKJVo044YUMttggiBkmp6KFXw1oII24oYhjiDByaKOOHcp3Y5BD/njikSkO+eBREQAAOw==",
@@ -28,7 +22,6 @@
"NODE_SELECTED_TITLE_COLOR": "#000",
"NODE_TEXT_SIZE": 14,
"NODE_TEXT_COLOR": "#444",
"NODE_TEXT_HIGHLIGHT_COLOR": "#1e293b",
"NODE_SUBTEXT_SIZE": 12,
"NODE_DEFAULT_COLOR": "#F7F7F7",
"NODE_DEFAULT_BGCOLOR": "#F5F5F5",

View File

@@ -1,22 +1,16 @@
<template>
<div
<Button
v-show="workspaceState.focusMode"
class="comfy-menu-hamburger no-drag"
class="comfy-menu-hamburger"
:style="positionCSS"
>
<Button
icon="pi pi-bars"
severity="secondary"
text
size="large"
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
:aria-label="$t('menu.showMenu')"
aria-live="assertive"
@click="exitFocusMode"
@contextmenu="showNativeMenu"
/>
<div v-show="menuSetting !== 'Bottom'" class="window-actions-spacer" />
</div>
icon="pi pi-bars"
severity="secondary"
text
size="large"
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
@click="exitFocusMode"
@contextmenu="showNativeMenu"
/>
</template>
<script setup lang="ts">
@@ -57,6 +51,8 @@ const positionCSS = computed<CSSProperties>(() =>
<style scoped>
.comfy-menu-hamburger {
@apply pointer-events-auto fixed z-[9999] flex flex-row;
pointer-events: auto;
position: fixed;
z-index: 9999;
}
</style>

View File

@@ -3,7 +3,6 @@
class="batch-count"
:class="props.class"
v-tooltip.bottom="$t('menu.batchCount')"
:aria-label="$t('menu.batchCount')"
>
<InputNumber
class="w-14"

View File

@@ -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"
@@ -39,7 +39,6 @@
:severity="executingPrompt ? 'danger' : 'secondary'"
:disabled="!executingPrompt"
text
:aria-label="$t('menu.interrupt')"
@click="() => commandStore.execute('Comfy.Interrupt')"
>
</Button>
@@ -49,7 +48,6 @@
:severity="hasPendingTasks ? 'danger' : 'secondary'"
:disabled="!hasPendingTasks"
text
:aria-label="$t('sideToolbar.queueTab.clearPendingTasks')"
@click="() => commandStore.execute('Comfy.ClearPendingTasks')"
/>
</ButtonGroup>

View File

@@ -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 () => {

View File

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

View File

@@ -12,14 +12,12 @@
:data-test="src"
class="comfy-image-blur"
:style="{ 'background-image': `url(${src})` }"
:alt="alt"
/>
<img
:src="src"
@error="handleImageError"
class="comfy-image-main"
:class="[...classArray]"
:alt="alt"
/>
</span>
<div v-if="imageBroken" class="broken-image-placeholder">
@@ -36,11 +34,9 @@ const props = withDefaults(
src: string
class?: string | string[] | object
contain: boolean
alt?: string
}>(),
{
contain: false,
alt: 'Image content'
contain: false
}
)

View File

@@ -26,7 +26,6 @@
:options="colorOptions"
optionLabel="name"
dataKey="value"
:allow-empty="false"
>
<template #option="slotProps">
<div

View File

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

View File

@@ -15,7 +15,7 @@
:label="$t('g.download') + ' (' + fileSize + ')'"
size="small"
outlined
:disabled="!!props.error"
:disabled="props.error"
:title="props.url"
@click="download.triggerBrowserDownload"
/>

View File

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

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

View File

@@ -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">&nbsp;</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>

View File

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

View File

@@ -19,7 +19,6 @@
v-for="device in props.stats.devices"
:key="device.index"
:header="device.name"
:value="device.index"
>
<DeviceInfo :device="device" />
</TabPanel>

View File

@@ -8,11 +8,9 @@
selectionMode="single"
:pt="{
nodeLabel: 'tree-explorer-node-label',
nodeContent: ({ context }) => ({
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) => {
@@ -154,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) {

View File

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

View File

@@ -5,20 +5,12 @@
:message="props.error.exception_message"
/>
<div class="comfy-error-report">
<div class="flex gap-2 justify-center">
<Button
v-show="!reportOpen"
text
:label="$t('g.showReport')"
@click="showReport"
/>
<Button
v-show="!sendReportOpen"
text
:label="$t('issueReport.helpFix')"
@click="showSendReport"
/>
</div>
<Button
v-show="!reportOpen"
:label="$t('g.showReport')"
@click="showReport"
text
/>
<template v-if="reportOpen">
<Divider />
<ScrollPanel style="width: 100%; height: 400px; max-width: 80vw">
@@ -26,14 +18,9 @@
</ScrollPanel>
<Divider />
</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">
<ReportIssueButton v-if="showSendError" :error="props.error" />
<FindIssueButton
:errorMessage="props.error.exception_message"
:repoOwner="repoOwner"
@@ -54,18 +41,16 @@ import Button from 'primevue/button'
import Divider from 'primevue/divider'
import ScrollPanel from 'primevue/scrollpanel'
import { useToast } from 'primevue/usetoast'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { onMounted, ref } from 'vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import FindIssueButton from '@/components/dialog/content/error/FindIssueButton.vue'
import ReportIssueButton from '@/components/dialog/content/error/ReportIssueButton.vue'
import { useCopyToClipboard } from '@/hooks/clipboardHooks'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import type { ExecutionErrorWsMessage, SystemStats } from '@/types/apiTypes'
import type { ReportField } from '@/types/issueReportTypes'
import ReportIssuePanel from './error/ReportIssuePanel.vue'
import { isElectron } from '@/utils/envUtil'
const props = defineProps<{
error: ExecutionErrorWsMessage
@@ -78,24 +63,9 @@ const reportOpen = ref(false)
const showReport = () => {
reportOpen.value = true
}
const sendReportOpen = ref(false)
const showSendReport = () => {
sendReportOpen.value = true
}
const toast = useToast()
const { t } = useI18n()
const showSendError = isElectron()
const stackTraceField = computed<ReportField>(() => {
return {
label: t('issueReport.stackTrace'),
value: 'StackTrace',
optIn: true,
getData: () => ({
nodeType: props.error.node_type,
stackTrace: props.error.traceback?.join('\n')
})
}
})
const toast = useToast()
onMounted(async () => {
try {

View File

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

View File

@@ -6,7 +6,6 @@
v-model:modelValue="searchQuery"
@search="handleSearch"
:placeholder="$t('g.searchSettings') + '...'"
:debounce-time="128"
/>
<Listbox
v-model="activeCategory"
@@ -15,7 +14,7 @@
scrollHeight="100%"
:optionDisabled="
(option: SettingTreeNode) =>
!queryIsEmpty && !searchResultsCategories.has(option.label)
inSearch && !searchResultsCategories.has(option.label)
"
class="border-none w-full"
/>
@@ -264,8 +263,9 @@ const handleSearch = (query: string) => {
activeCategory.value = null
}
const queryIsEmpty = computed(() => searchQuery.value.length === 0)
const inSearch = computed(() => !queryIsEmpty.value && !searchInProgress.value)
const inSearch = computed(
() => searchQuery.value.length > 0 && !searchInProgress.value
)
const tabValue = computed(() =>
inSearch.value ? 'Search Results' : activeCategory.value?.label
)

View File

@@ -0,0 +1,52 @@
<template>
<Button
@click="reportIssue"
:label="$t('g.reportIssue')"
:severity="submitted ? 'success' : 'secondary'"
:icon="icon"
:disabled="submitted"
v-tooltip="$t('g.reportIssueTooltip')"
>
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { useToast } from 'primevue/usetoast'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { ExecutionErrorWsMessage } from '@/types/apiTypes'
import { electronAPI } from '@/utils/envUtil'
const { error } = defineProps<{
error: ExecutionErrorWsMessage
}>()
const { t } = useI18n()
const toast = useToast()
const submitting = ref(false)
const submitted = ref(false)
const icon = computed(
() => `pi ${submitting.value ? 'pi-spin pi-spinner' : 'pi-send'}`
)
const reportIssue = async () => {
if (submitting.value) return
submitting.value = true
try {
await electronAPI().sendErrorToSentry(error.exception_message, {
stackTrace: error.traceback?.join('\n'),
nodeType: error.node_type
})
submitted.value = true
toast.add({
severity: 'success',
summary: t('g.reportSent'),
life: 3000
})
} finally {
submitting.value = false
}
}
</script>

View File

@@ -1,251 +0,0 @@
<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"
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>
</div>
</Panel>
</Form>
</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 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 { 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'
const ISSUE_NAME = 'User reported issue'
const props = defineProps<IssueReportPanelProps>()
const { defaultFields = ['Workflow', 'Logs', 'SystemStats', 'Settings'] } =
props
const { t } = useI18n()
const toast = useToast()
const selection = ref<string[]>([])
const contactPrefs = ref<string[]>([])
const submitted = ref(false)
const contactCheckboxes = [
{ label: t('issueReport.contactFollowUp'), value: 'followUp' },
{ label: t('issueReport.notifyResolve'), value: 'notifyOnResolution' }
]
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 fields = computed(() => [
...defaultFieldsConfig.filter(({ value }) =>
defaultFields.includes(value as DefaultField)
),
...(props.extraFields ?? [])
])
const createUser = (formData: IssueReportFormData): User => ({
email: formData.contactInfo || undefined
})
const createExtraData = async (formData: IssueReportFormData) => {
const result = {}
const isChecked = (fieldValue: string) => formData[fieldValue]
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) }
}
})
)
return result
}
const createCaptureContext = async (
formData: IssueReportFormData
): Promise<CaptureContext> => {
return {
user: createUser(formData),
level: 'error',
tags: {
errorType: props.errorType,
followUp: formData.contactInfo ? formData.followUp : false,
notifyOnResolution: formData.contactInfo
? formData.notifyOnResolution
: false,
isElectron: isElectron(),
...props.tags
},
extra: {
details: formData.details,
...(await createExtraData(formData))
}
}
}
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
})
}
}
}
</script>

View File

@@ -1,315 +0,0 @@
// @ts-strict-ignore
import { Form } from '@primevue/forms'
import { mount } from '@vue/test-utils'
import Checkbox from 'primevue/checkbox'
import PrimeVue from 'primevue/config'
import InputText from 'primevue/inputtext'
import Textarea from 'primevue/textarea'
import Tooltip from 'primevue/tooltip'
import { describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import enMesages from '@/locales/en/main.json'
import { IssueReportPanelProps } 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
}
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: enMesages
}
})
vi.mock('primevue/usetoast', () => ({
useToast: vi.fn(() => ({
add: vi.fn()
}))
}))
vi.mock('@/scripts/api', () => ({
api: {
getLogs: vi.fn().mockResolvedValue('mock logs'),
getSystemStats: vi.fn().mockResolvedValue('mock stats'),
getSettings: vi.fn().mockResolvedValue('mock settings')
}
}))
vi.mock('@/scripts/app', () => ({
app: {
graph: {
asSerialisable: vi.fn().mockReturnValue({})
}
}
}))
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()
})
const mountComponent = (props: IssueReportPanelProps, options = {}): any => {
return mount(ReportIssuePanel, {
global: {
plugins: [PrimeVue, i18n],
directives: { tooltip: Tooltip }
},
props,
...options
})
}
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.findComponent(InputText).exists()).toBe(true)
expect(wrapper.findComponent(Textarea).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)
}
})
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')
})
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.')
})
it('set contact preferences back to false if email is removed', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const input = wrapper.findComponent(InputText)
// 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('renders with overridden default fields', () => {
const wrapper = mountComponent({
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')
})
it('renders additional fields when extraFields prop is provided', () => {
const wrapper = mountComponent({
errorType: 'Test Error',
extraFields: CUSTOM_FIELDS
})
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)
// Verify none of the optional fields were included
for (const field of DEFAULT_FIELDS) {
expect(context.extra[field]).toBeUndefined()
}
})
it.each([
{
checkbox: 'Logs',
apiMethod: 'getLogs',
expectedKey: 'Logs',
mockValue: 'mock logs'
},
{
checkbox: 'SystemStats',
apiMethod: 'getSystemStats',
expectedKey: 'SystemStats',
mockValue: 'mock stats'
},
{
checkbox: 'Settings',
apiMethod: 'getSettings',
expectedKey: 'Settings',
mockValue: 'mock settings'
}
])(
'submits $checkbox data when 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)
}
)
it('submits workflow when the Workflow checkbox is selected', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const { app } = (await import('@/scripts/app')) as any
const mockWorkflow = { nodes: [], edges: [] }
vi.spyOn(app.graph, 'asSerialisable').mockReturnValue(mockWorkflow)
await findAndUpdateCheckbox(wrapper, 'Workflow')
const context = await submitForm(wrapper)
expect(context.extra.Workflow).toEqual(mockWorkflow)
})
})

View File

@@ -13,13 +13,13 @@
optionValue="id"
/>
<Button
icon="pi pi-file-export"
icon="pi pi-download"
text
:title="$t('g.export')"
:title="$t('g.download')"
@click="colorPaletteService.exportColorPalette(activePaletteId)"
/>
<Button
icon="pi pi-file-import"
icon="pi pi-upload"
text
:title="$t('g.import')"
@click="importCustomPalette"

View File

@@ -13,9 +13,6 @@
<BottomPanel />
</template>
<template #graph-canvas-panel>
<SecondRowWorkflowTabs
v-if="workflowTabsPosition === 'Topbar (2nd-row)'"
/>
<GraphCanvasMenu v-if="canvasMenuEnabled" />
</template>
</LiteGraphCanvasSplitterOverlay>
@@ -51,13 +48,12 @@ import NodeTooltip from '@/components/graph/NodeTooltip.vue'
import TitleEditor from '@/components/graph/TitleEditor.vue'
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
import { CORE_SETTINGS } from '@/constants/coreSettings'
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 { setStorageValue } from '@/scripts/utils'
import { IS_CONTROL_WIDGET, updateControlWidgetLabel } from '@/scripts/widgets'
import { useColorPaletteService } from '@/services/colorPaletteService'
import { useLitegraphService } from '@/services/litegraphService'
@@ -87,37 +83,11 @@ const modelToNodeStore = useModelToNodeStore()
const betaMenuEnabled = computed(
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
)
const workflowTabsPosition = computed(() =>
settingStore.get('Comfy.Workflow.WorkflowTabsPosition')
)
const canvasMenuEnabled = computed(() =>
settingStore.get('Comfy.Graph.CanvasMenu')
)
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) {
@@ -387,18 +357,6 @@ onMounted(async () => {
'Comfy.CustomColorPalettes'
)
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(
() => settingStore.get('Comfy.Locale'),

View File

@@ -6,7 +6,6 @@
severity="secondary"
icon="pi pi-plus"
v-tooltip.left="t('graphCanvasMenu.zoomIn')"
:aria-label="$t('graphCanvasMenu.zoomIn')"
@mousedown="repeat('Comfy.Canvas.ZoomIn')"
@mouseup="stopRepeat"
/>
@@ -14,7 +13,6 @@
severity="secondary"
icon="pi pi-minus"
v-tooltip.left="t('graphCanvasMenu.zoomOut')"
:aria-label="$t('graphCanvasMenu.zoomOut')"
@mousedown="repeat('Comfy.Canvas.ZoomOut')"
@mouseup="stopRepeat"
/>
@@ -22,7 +20,6 @@
severity="secondary"
icon="pi pi-expand"
v-tooltip.left="t('graphCanvasMenu.fitView')"
:aria-label="$t('graphCanvasMenu.fitView')"
@click="() => commandStore.execute('Comfy.Canvas.FitView')"
/>
<Button
@@ -33,12 +30,6 @@
(canvasStore.canvas?.read_only ? 'panMode' : 'selectMode')
) + ' (Space)'
"
:aria-label="
t(
'graphCanvasMenu.' +
(canvasStore.canvas?.read_only ? 'panMode' : 'selectMode')
)
"
@click="() => commandStore.execute('Comfy.Canvas.ToggleLock')"
>
<template #icon>
@@ -52,7 +43,6 @@
severity="secondary"
:icon="linkHidden ? 'pi pi-eye-slash' : 'pi pi-eye'"
v-tooltip.left="t('graphCanvasMenu.toggleLinkVisibility')"
:aria-label="$t('graphCanvasMenu.toggleLinkVisibility')"
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
data-testid="toggle-link-visibility-button"
/>

View File

@@ -59,19 +59,10 @@
</h4>
<ul class="list-disc pl-6 space-y-1">
<li>
{{
$t('install.settings.dataCollectionDialog.collect.errorReports')
}}
{{ $t('install.settings.dataCollectionDialog.errorReports') }}
</li>
<li>
{{ $t('install.settings.dataCollectionDialog.collect.systemInfo') }}
</li>
<li>
{{
$t(
'install.settings.dataCollectionDialog.collect.userJourneyEvents'
)
}}
{{ $t('install.settings.dataCollectionDialog.systemInfo') }}
</li>
</ul>
@@ -81,43 +72,25 @@
<ul class="list-disc pl-6 space-y-1">
<li>
{{
$t(
'install.settings.dataCollectionDialog.doNotCollect.personalInformation'
)
$t('install.settings.dataCollectionDialog.personalInformation')
}}
</li>
<li>
{{ $t('install.settings.dataCollectionDialog.workflowContents') }}
</li>
<li>
{{
$t('install.settings.dataCollectionDialog.fileSystemInformation')
}}
</li>
<li>
{{
$t(
'install.settings.dataCollectionDialog.doNotCollect.workflowContents'
)
}}
</li>
<li>
{{
$t(
'install.settings.dataCollectionDialog.doNotCollect.fileSystemInformation'
)
}}
</li>
<li>
{{
$t(
'install.settings.dataCollectionDialog.doNotCollect.customNodeConfigurations'
'install.settings.dataCollectionDialog.customNodeConfigurations'
)
}}
</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 +103,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

View File

@@ -130,13 +130,12 @@
</template>
<script setup lang="ts">
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
import Tag from 'primevue/tag'
import ToggleSwitch from 'primevue/toggleswitch'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { electronAPI } from '@/utils/envUtil'
import { TorchDeviceType, electronAPI } from '@/utils/envUtil'
const { t } = useI18n()
@@ -159,8 +158,8 @@ const pickGpu = (value: typeof selected.value) => {
}
</script>
<style scoped>
.p-tag {
<style lang="postcss">
:root {
--p-tag-gap: 0.5rem;
}

View File

@@ -1,40 +0,0 @@
<template>
<Tag :icon :severity :value />
</template>
<script setup lang="ts">
import { PrimeIcons, type PrimeIconsOptions } from '@primevue/core/api'
import Tag, { TagProps } from 'primevue/tag'
import { ref, watch } from 'vue'
import { t } from '@/i18n'
// Properties
const props = defineProps<{
error: boolean
refreshing?: boolean
}>()
// Bindings
const icon = ref<string>(null)
const severity = ref<TagProps['severity']>(null)
const value = ref<PrimeIconsOptions[keyof PrimeIconsOptions]>(null)
const updateBindings = () => {
if (props.refreshing) {
icon.value = PrimeIcons.QUESTION
severity.value = 'info'
value.value = t('maintenance.refreshing')
} else if (props.error) {
icon.value = PrimeIcons.TIMES
severity.value = 'danger'
value.value = t('g.error')
} else {
icon.value = PrimeIcons.CHECK
severity.value = 'success'
value.value = t('maintenance.OK')
}
}
watch(props, updateBindings, { deep: true })
</script>

View File

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

View File

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

View File

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

View File

@@ -1,45 +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'
import { MaintenanceTaskState } from '@/stores/maintenanceTaskStore'
// 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?: MaintenanceTaskState
loading?: MaybeRef<boolean>
}>()
</script>

View File

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

View File

@@ -19,13 +19,7 @@
class="filter-button z-10"
@click="nodeSearchFilterVisible = true"
/>
<Dialog
v-model:visible="nodeSearchFilterVisible"
class="min-w-96"
dismissable-mask
modal
@hide="reFocusInput"
>
<Dialog v-model:visible="nodeSearchFilterVisible" class="min-w-96">
<template #header>
<h3>Add node filter condition</h3>
</template>
@@ -146,6 +140,7 @@ onMounted(reFocusInput)
const onAddFilter = (filterAndValue: FilterAndValue) => {
nodeSearchFilterVisible.value = false
emit('addFilter', filterAndValue)
reFocusInput()
}
const onRemoveFilter = (event: Event, filterAndValue: FilterAndValue) => {
event.stopPropagation()

View File

@@ -17,7 +17,7 @@
</div>
<div
v-if="showCategory"
class="option-category font-light text-sm text-muted overflow-hidden text-ellipsis whitespace-nowrap"
class="option-category font-light text-sm text-gray-400 overflow-hidden text-ellipsis whitespace-nowrap"
>
{{ nodeDef.category.replaceAll('/', ' > ') }}
</div>

View File

@@ -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: 64px;
--sidebar-icon-size: 1.5rem;
}
:root .small-sidebar {
--sidebar-width: 40px;
--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 {

View File

@@ -11,15 +11,14 @@
import { computed } from 'vue'
import { useCommandStore } from '@/stores/commandStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useSettingStore } from '@/stores/settingStore'
import SidebarIcon from './SidebarIcon.vue'
const colorPaletteStore = useColorPaletteStore()
const settingStore = useSettingStore()
const currentTheme = computed(() => settingStore.get('Comfy.ColorPalette'))
const icon = computed(() =>
colorPaletteStore.completedActivePalette.light_theme
? 'pi pi-sun'
: 'pi pi-moon'
currentTheme.value !== 'light' ? 'pi pi-moon' : 'pi pi-sun'
)
const commandStore = useCommandStore()

View File

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

View File

@@ -4,11 +4,11 @@
:class="props.class"
>
<div class="comfy-vue-side-bar-header">
<Toolbar class="border-x-0 border-t-0 rounded-none px-2 py-1 min-h-8">
<Toolbar
class="flex-shrink-0 border-x-0 border-t-0 rounded-none px-2 py-1 min-h-8"
>
<template #start>
<span class="text-xs 2xl:text-sm truncate" :title="props.title">
{{ props.title.toUpperCase() }}
</span>
<span class="text-sm">{{ props.title.toUpperCase() }}</span>
</template>
<template #end>
<slot name="tool-buttons"></slot>
@@ -37,8 +37,4 @@ const props = defineProps<{
:deep(.p-toolbar-end) .p-button {
@apply py-1 2xl:py-2;
}
:deep(.p-toolbar-start) {
@apply min-w-0 flex-1 overflow-hidden;
}
</style>

View File

@@ -24,7 +24,6 @@
:key="item.url"
:src="item.url"
:contain="false"
:alt="item.filename"
class="galleria-image"
v-if="item.isImage"
/>

View File

@@ -5,7 +5,6 @@
:src="result.url"
class="task-output-image"
:contain="imageFit === 'contain'"
:alt="result.filename"
/>
<ResultVideo v-else-if="result.isVideo" :result="result" />
<div v-else class="task-result-preview">

View File

@@ -3,7 +3,7 @@
<template #header>
<div class="flex items-center justify-center">
<div
class="relative overflow-hidden rounded-t-lg cursor-pointer w-64 h-64"
class="relative overflow-hidden rounded-lg cursor-pointer w-64 h-64"
>
<img
v-if="!imageError"
@@ -13,7 +13,7 @@
: `api/workflow_templates/${props.moduleName}/${props.workflowName}.jpg`
"
@error="imageError = true"
class="w-64 h-64 rounded-t-lg object-cover thumbnail"
class="w-64 h-64 rounded-lg object-cover thumbnail"
/>
<div v-else class="w-64 h-64 content-center text-center">
<i class="pi pi-file" style="font-size: 4rem"></i>

View File

@@ -24,7 +24,7 @@
:key="selectedTab.moduleName"
>
<template #item="slotProps">
<div @click="loadWorkflow(slotProps.data)" class="p-2">
<div @click="loadWorkflow(slotProps.data)">
<TemplateWorkflowCard
:moduleName="selectedTab.moduleName"
:workflowName="slotProps.data"

View File

@@ -3,7 +3,6 @@
v-show="bottomPanelStore.bottomPanelTabs.length > 0"
severity="secondary"
text
:aria-label="$t('menu.toggleBottomPanel')"
@click="bottomPanelStore.toggleBottomPanel"
v-tooltip="{ value: $t('menu.toggleBottomPanel'), showDelay: 300 }"
>

View File

@@ -19,7 +19,7 @@
<span class="p-menubar-item-label">{{ item.label }}</span>
<span
v-if="item?.comfyCommand?.keybinding"
class="ml-auto border border-surface rounded text-muted text-xs text-nowrap p-1 keybinding-tag"
class="ml-auto border border-surface rounded text-muted text-xs p-1 keybinding-tag"
>
{{ item.comfyCommand.keybinding.combo.toString() }}
</span>

View File

@@ -1,15 +0,0 @@
<template>
<div class="absolute top-0 left-0 w-auto max-w-full pointer-events-auto">
<WorkflowTabs />
</div>
</template>
<script setup lang="ts">
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
</script>
<style scoped>
:deep(.workflow-tabs) {
background-color: var(--comfy-menu-bg);
}
</style>

View File

@@ -3,44 +3,34 @@
<div
ref="topMenuRef"
class="comfyui-menu flex items-center"
v-show="showTopMenu"
v-show="betaMenuEnabled && !workspaceState.focusMode"
:class="{ dropzone: isDropZone, 'dropzone-active': isDroppable }"
>
<h1 class="comfyui-logo mx-2 app-drag">ComfyUI</h1>
<h1 class="comfyui-logo mx-2">ComfyUI</h1>
<CommandMenubar />
<div class="flex-grow min-w-0 app-drag h-full">
<Divider layout="vertical" class="mx-2" />
<div class="flex-grow">
<WorkflowTabs v-if="workflowTabsPosition === 'Topbar'" />
</div>
<div class="comfyui-menu-right" ref="menuRight"></div>
<Actionbar />
<BottomPanelToggleButton class="flex-shrink-0" />
<BottomPanelToggleButton />
<Button
class="flex-shrink-0"
icon="pi pi-bars"
severity="secondary"
text
v-tooltip="{ value: $t('menu.hideMenu'), showDelay: 300 }"
:aria-label="$t('menu.hideMenu')"
@click="workspaceState.focusMode = true"
@contextmenu="showNativeMenu"
/>
<div
v-show="menuSetting !== 'Bottom'"
class="window-actions-spacer flex-shrink-0"
/>
</div>
</teleport>
<!-- Virtual top menu for native window (drag handle) -->
<div
v-show="isNativeWindow && !showTopMenu"
class="fixed top-0 left-0 app-drag w-full h-[var(--comfy-topbar-height)]"
/>
</template>
<script setup lang="ts">
import { useEventBus } from '@vueuse/core'
import Button from 'primevue/button'
import Divider from 'primevue/divider'
import { computed, onMounted, provide, ref } from 'vue'
import Actionbar from '@/components/actionbar/ComfyActionbar.vue'
@@ -50,27 +40,21 @@ 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, showNativeMenu } from '@/utils/envUtil'
import { showNativeMenu } from '@/utils/envUtil'
const workspaceState = useWorkspaceStore()
const settingStore = useSettingStore()
const workflowTabsPosition = computed(() =>
settingStore.get('Comfy.Workflow.WorkflowTabsPosition')
)
const menuSetting = computed(() => settingStore.get('Comfy.UseNewMenu'))
const betaMenuEnabled = computed(() => menuSetting.value !== 'Disabled')
const betaMenuEnabled = computed(
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
)
const teleportTarget = computed(() =>
settingStore.get('Comfy.UseNewMenu') === 'Top'
? '.comfyui-body-top'
: '.comfyui-body-bottom'
)
const isNativeWindow = computed(
() =>
isElectron() && settingStore.get('Comfy-Desktop.WindowStyle') === 'custom'
)
const showTopMenu = computed(
() => betaMenuEnabled.value && !workspaceState.focusMode
)
const menuRight = ref<HTMLDivElement | null>(null)
// Menu-right holds legacy topbar elements attached by custom scripts
@@ -91,20 +75,11 @@ eventBus.on((event: string, payload: any) => {
isDroppable.value = payload.isOverlapping && payload.isDragging
}
})
onMounted(() => {
if (isElectron()) {
electronAPI().changeTheme({
height: topMenuRef.value.getBoundingClientRect().height
})
}
})
</script>
<style scoped>
.comfyui-menu {
width: 100vw;
height: var(--comfy-topbar-height);
background: var(--comfy-menu-bg);
color: var(--fg-color);
box-shadow: var(--bar-shadow);
@@ -114,6 +89,7 @@ onMounted(() => {
z-index: 1000;
order: 0;
grid-column: 1/-1;
max-height: 90vh;
}
.comfyui-menu.dropzone {

View File

@@ -1,48 +1,34 @@
<template>
<div class="workflow-tabs-container flex flex-row max-w-full h-full">
<ScrollPanel
class="overflow-hidden no-drag"
:pt:content="{
class: 'p-0 w-full',
onwheel: handleWheel
}"
pt:barX="h-1"
>
<SelectButton
class="workflow-tabs bg-transparent"
:class="props.class"
:modelValue="selectedWorkflow"
@update:modelValue="onWorkflowChange"
:options="options"
optionLabel="label"
dataKey="value"
>
<template #option="{ option }">
<WorkflowTab
@contextmenu="showContextMenu($event, option)"
@click.middle="onCloseWorkflow(option)"
:workflow-option="option"
/>
</template>
</SelectButton>
</ScrollPanel>
<Button
v-tooltip="{ value: $t('sideToolbar.newBlankWorkflow'), showDelay: 300 }"
class="new-blank-workflow-button flex-shrink-0 no-drag"
icon="pi pi-plus"
text
severity="secondary"
:aria-label="$t('sideToolbar.newBlankWorkflow')"
@click="() => commandStore.execute('Comfy.NewBlankWorkflow')"
/>
<ContextMenu ref="menu" :model="contextMenuItems" />
</div>
<SelectButton
class="workflow-tabs bg-transparent inline"
:class="props.class"
:modelValue="selectedWorkflow"
@update:modelValue="onWorkflowChange"
:options="options"
optionLabel="label"
dataKey="value"
>
<template #option="{ option }">
<WorkflowTab
@contextmenu="showContextMenu($event, option)"
@click.middle="onCloseWorkflow(option)"
:workflow-option="option"
/>
</template>
</SelectButton>
<Button
class="new-blank-workflow-button"
icon="pi pi-plus"
text
severity="secondary"
@click="() => commandStore.execute('Comfy.NewBlankWorkflow')"
/>
<ContextMenu ref="menu" :model="contextMenuItems" />
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import ContextMenu from 'primevue/contextmenu'
import ScrollPanel from 'primevue/scrollpanel'
import SelectButton from 'primevue/selectbutton'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -50,7 +36,7 @@ import { useI18n } from 'vue-i18n'
import WorkflowTab from '@/components/topbar/WorkflowTab.vue'
import { useWorkflowService } from '@/services/workflowService'
import { useCommandStore } from '@/stores/commandStore'
import { ComfyWorkflow, useWorkflowBookmarkStore } from '@/stores/workflowStore'
import { ComfyWorkflow } from '@/stores/workflowStore'
import { useWorkflowStore } from '@/stores/workflowStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
@@ -67,7 +53,6 @@ const { t } = useI18n()
const workspaceStore = useWorkspaceStore()
const workflowStore = useWorkflowStore()
const workflowService = useWorkflowService()
const workflowBookmarkStore = useWorkflowBookmarkStore()
const rightClickedTab = ref<WorkflowOption>(null)
const menu = ref()
@@ -155,56 +140,26 @@ const contextMenuItems = computed(() => {
...options.value.slice(0, index)
]),
disabled: options.value.length <= 1
},
{
label: workflowBookmarkStore.isBookmarked(tab.workflow.path)
? t('tabMenu.removeFromBookmarks')
: t('tabMenu.addToBookmarks'),
command: () => workflowBookmarkStore.toggleBookmarked(tab.workflow.path),
disabled: tab.workflow.isTemporary
}
]
})
const commandStore = useCommandStore()
// Horizontal scroll on wheel
const handleWheel = (event: WheelEvent) => {
const scrollElement = event.currentTarget as HTMLElement
const scrollAmount = event.deltaX || event.deltaY
scrollElement.scroll({
left: scrollElement.scrollLeft + scrollAmount
})
}
</script>
<style scoped>
:deep(.p-togglebutton) {
@apply p-0 bg-transparent rounded-none flex-shrink-0 relative border-0 border-r border-solid;
border-right-color: var(--border-color);
}
:deep(.p-togglebutton::before) {
@apply hidden;
}
:deep(.p-togglebutton:first-child) {
@apply border-l border-solid;
border-left-color: var(--border-color);
}
:deep(.p-togglebutton:not(:first-child)) {
@apply border-l-0;
:deep(.p-togglebutton) {
@apply p-0 bg-transparent rounded-none flex-shrink-0 relative;
}
:deep(.p-togglebutton.p-togglebutton-checked) {
@apply border-b border-solid h-full;
@apply border-b-2;
border-bottom-color: var(--p-button-text-primary-color);
}
:deep(.p-togglebutton:not(.p-togglebutton-checked)) {
@apply opacity-75;
}
:deep(.p-togglebutton-checked) .close-button,
:deep(.p-togglebutton:hover) .close-button {
@apply visible;
@@ -217,18 +172,4 @@ const handleWheel = (event: WheelEvent) => {
:deep(.p-togglebutton) .close-button {
@apply invisible;
}
:deep(.p-scrollpanel-content) {
@apply h-full;
}
/* Scrollbar half opacity to avoid blocking the active tab bottom border */
:deep(.p-scrollpanel:hover .p-scrollpanel-bar),
:deep(.p-scrollpanel:active .p-scrollpanel-bar) {
@apply opacity-50;
}
:deep(.p-selectbutton) {
@apply rounded-none h-full;
}
</style>

View File

@@ -4,7 +4,7 @@ import github from '@/assets/palettes/github.json'
import light from '@/assets/palettes/light.json'
import nord from '@/assets/palettes/nord.json'
import solarized from '@/assets/palettes/solarized.json'
import type { ColorPalettes, CompletedPalette } from '@/types/colorPaletteTypes'
import type { ColorPalettes } from '@/types/colorPaletteTypes'
export const CORE_COLOR_PALETTES: ColorPalettes = {
dark,
@@ -15,6 +15,4 @@ export const CORE_COLOR_PALETTES: ColorPalettes = {
github
} as const
export const DEFAULT_COLOR_PALETTE: CompletedPalette = dark
export const DEFAULT_DARK_COLOR_PALETTE: CompletedPalette = dark
export const DEFAULT_LIGHT_COLOR_PALETTE: CompletedPalette = light
export const DEFAULT_COLOR_PALETTE = dark

View File

@@ -95,7 +95,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
alt: true
},
commandId: 'Comfy.Canvas.ZoomIn',
targetElementId: 'graph-canvas'
targetSelector: '#graph-canvas'
},
{
combo: {
@@ -104,7 +104,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
shift: true
},
commandId: 'Comfy.Canvas.ZoomIn',
targetElementId: 'graph-canvas'
targetSelector: '#graph-canvas'
},
// For number pad '+'
{
@@ -113,7 +113,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
alt: true
},
commandId: 'Comfy.Canvas.ZoomIn',
targetElementId: 'graph-canvas'
targetSelector: '#graph-canvas'
},
{
combo: {
@@ -121,21 +121,21 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
alt: true
},
commandId: 'Comfy.Canvas.ZoomOut',
targetElementId: 'graph-canvas'
targetSelector: '#graph-canvas'
},
{
combo: {
key: '.'
},
commandId: 'Comfy.Canvas.FitView',
targetElementId: 'graph-canvas'
targetSelector: '#graph-canvas'
},
{
combo: {
key: 'p'
},
commandId: 'Comfy.Canvas.ToggleSelected.Pin',
targetElementId: 'graph-canvas'
targetSelector: '#graph-canvas'
},
{
combo: {
@@ -143,7 +143,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
alt: true
},
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Collapse',
targetElementId: 'graph-canvas'
targetSelector: '#graph-canvas'
},
{
combo: {
@@ -151,7 +151,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
ctrl: true
},
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Bypass',
targetElementId: 'graph-canvas'
targetSelector: '#graph-canvas'
},
{
combo: {
@@ -159,7 +159,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
ctrl: true
},
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Mute',
targetElementId: 'graph-canvas'
targetSelector: '#graph-canvas'
},
{
combo: {

View File

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

View File

@@ -85,8 +85,7 @@ export const CORE_SETTINGS: SettingParams[] = [
name: 'Sidebar size',
type: 'combo',
options: ['normal', 'small'],
// Default to small if the window is less than 1536px(2xl) wide.
defaultValue: () => (window.innerWidth < 1536 ? 'small' : 'normal')
defaultValue: () => (window.innerWidth < 1600 ? 'small' : 'normal')
},
{
id: 'Comfy.TextareaWidget.FontSize',
@@ -230,8 +229,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',
@@ -401,10 +399,8 @@ export const CORE_SETTINGS: SettingParams[] = [
id: 'Comfy.Workflow.WorkflowTabsPosition',
name: 'Opened workflows position',
type: 'combo',
options: ['Sidebar', 'Topbar', 'Topbar (2nd-row)'],
// Default to topbar (2nd-row) if the window is less than 1536px(2xl) wide.
defaultValue: () =>
window.innerWidth < 1536 ? 'Topbar (2nd-row)' : 'Topbar'
options: ['Sidebar', 'Topbar'],
defaultValue: 'Topbar'
},
{
id: 'Comfy.Graph.CanvasMenu',
@@ -427,16 +423,7 @@ export const CORE_SETTINGS: SettingParams[] = [
name: 'Keybindings unset by the user',
type: 'hidden',
defaultValue: [] as Keybinding[],
versionAdded: '1.3.7',
versionModified: '1.7.3',
migrateDeprecatedValue: (value: any[]) => {
return value.map((keybinding) => {
if (keybinding['targetSelector'] === '#graph-canvas') {
keybinding['targetElementId'] = 'graph-canvas'
}
return keybinding
})
}
versionAdded: '1.3.7'
},
{
id: 'Comfy.Keybinding.NewBindings',

View File

@@ -1,144 +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.',
description:
'Git is required to download and manage custom nodes and other extensions. This fixer simply opens the download page in your browser. You must download and install git manually.',
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

View File

@@ -1,8 +1,6 @@
import { t } from '@/i18n'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
import { useSettingStore } from '@/stores/settingStore'
import { useWorkflowStore } from '@/stores/workflowStore'
import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
;(async () => {
@@ -10,7 +8,6 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
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.
@@ -33,28 +30,10 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
{
id: 'Comfy-Desktop.SendStatistics',
category: ['Comfy-Desktop', 'General', 'Send Statistics'],
name: 'Send anonymous usage metrics',
name: 'Send anonymous crash reports',
type: 'boolean',
defaultValue: true,
onChange: onChangeRestartApp
},
{
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",
type: 'combo',
experimental: true,
defaultValue: 'default',
options: ['default', 'custom'],
onChange: (
newValue: 'default' | 'custom',
oldValue?: 'default' | 'custom'
) => {
if (!oldValue) return
electronAPI.Config.setWindowStyle(newValue)
}
}
],
@@ -115,6 +94,14 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
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',
@@ -144,32 +131,16 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
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'],
@@ -192,16 +163,6 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
}
],
keybindings: [
{
commandId: 'Workspace.CloseWorkflow',
combo: {
key: 'w',
ctrl: true
}
}
],
aboutPageBadges: [
{
label: 'ComfyUI_desktop v' + desktopAppVersion,

View File

@@ -6,6 +6,7 @@ import './electronAdapter'
import './groupNode'
import './groupNodeManage'
import './groupOptions'
import './invertMenuScrolling'
import './load3d'
import './maskeditor'
import './nodeTemplates'

View File

@@ -0,0 +1,41 @@
// @ts-strict-ignore
import { LiteGraph } from '@comfyorg/litegraph'
import { app } from '../../scripts/app'
// Inverts the scrolling of context menus
const id = 'Comfy.InvertMenuScrolling'
app.registerExtension({
name: id,
init() {
const ctxMenu = LiteGraph.ContextMenu
const replace = () => {
// @ts-expect-error
LiteGraph.ContextMenu = function (values, options) {
options = options || {}
if (options.scroll_speed) {
options.scroll_speed *= -1
} else {
options.scroll_speed = -0.1
}
return ctxMenu.call(this, values, options)
}
LiteGraph.ContextMenu.prototype = ctxMenu.prototype
}
app.ui.settings.addSetting({
id,
category: ['LiteGraph', 'Menu', 'InvertMenuScrolling'],
name: 'Invert Context Menu Scrolling',
type: 'boolean',
defaultValue: false,
onChange(value) {
if (value) {
replace()
} else {
LiteGraph.ContextMenu = ctxMenu
}
}
})
}
})

View File

@@ -2,7 +2,6 @@
import { IWidget } 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'
@@ -116,7 +115,8 @@ class Load3d {
stlLoader: STLLoader
currentModel: THREE.Object3D | null = null
originalModel: THREE.Object3D | THREE.BufferGeometry | GLTF | null = null
animationFrameId: number | null = null
node: any
private animationFrameId: number | null = null
gridHelper: THREE.GridHelper
lights: THREE.Light[] = []
clock: THREE.Clock
@@ -131,10 +131,6 @@ class Load3d {
currentUpDirection: 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z' =
'original'
originalRotation: THREE.Euler | null = null
viewHelper: ViewHelper
viewHelperContainer: HTMLDivElement
cameraSwitcherContainer: HTMLDivElement
gridSwitcherContainer: HTMLDivElement
constructor(container: Element | HTMLElement) {
this.scene = new THREE.Scene()
@@ -161,7 +157,6 @@ class Load3d {
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
@@ -208,143 +203,13 @@ class Load3d {
this.standardMaterial = this.createSTLMaterial()
this.createViewHelper(container)
this.createGridSwitcher(container)
this.createCameraSwitcher(container)
this.animate()
this.handleResize()
this.startAnimation()
}
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)
}
setFOV(fov: number) {
if (this.activeCamera === this.perspectiveCamera) {
this.perspectiveCamera.fov = fov
@@ -600,13 +465,6 @@ class Load3d {
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.handleResize()
}
@@ -643,16 +501,8 @@ class Load3d {
startAnimation() {
const animate = () => {
this.animationFrameId = requestAnimationFrame(animate)
const delta = this.clock.getDelta()
if (this.viewHelper.animating) {
this.viewHelper.update(delta)
}
this.renderer.clear()
this.controls.update()
this.renderer.render(this.scene, this.activeCamera)
this.viewHelper.render(this.renderer)
}
animate()
}
@@ -738,7 +588,6 @@ class Load3d {
}
this.controls.dispose()
this.viewHelper.dispose()
this.renderer.dispose()
this.renderer.domElement.remove()
this.scene.clear()
@@ -969,12 +818,10 @@ class Load3d {
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')
@@ -999,6 +846,44 @@ class Load3d {
})
}
setViewPosition(position: 'front' | 'top' | 'right' | 'isometric') {
if (!this.currentModel) {
return
}
const box = new THREE.Box3()
let center = new THREE.Vector3()
let size = new THREE.Vector3()
if (this.currentModel) {
box.setFromObject(this.currentModel)
box.getCenter(center)
box.getSize(size)
}
const maxDim = Math.max(size.x, size.y, size.z)
const distance = maxDim * 2
switch (position) {
case 'front':
this.activeCamera.position.set(0, 0, distance)
break
case 'top':
this.activeCamera.position.set(0, distance, 0)
break
case 'right':
this.activeCamera.position.set(distance, 0, 0)
break
case 'isometric':
this.activeCamera.position.set(distance, distance, distance)
break
}
this.activeCamera.lookAt(center)
this.controls.target.copy(center)
this.controls.update()
}
setBackgroundColor(color: string) {
this.renderer.setClearColor(new THREE.Color(color))
this.renderer.render(this.scene, this.activeCamera)
@@ -1135,28 +1020,16 @@ class Load3dAnimation extends Load3d {
})
}
startAnimation() {
const animate = () => {
this.animationFrameId = requestAnimationFrame(animate)
animate = () => {
requestAnimationFrame(this.animate)
if (this.currentAnimation && this.isAnimationPlaying) {
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)
}
this.viewHelper.render(this.renderer)
this.currentAnimation.update(delta)
}
animate()
this.controls.update()
this.renderer.render(this.scene, this.activeCamera)
}
}
@@ -1203,6 +1076,9 @@ function configureLoad3D(
load3d: Load3d,
loadFolder: 'input' | 'output',
modelWidget: IWidget,
showGrid: IWidget,
cameraType: IWidget,
view: IWidget,
material: IWidget,
bgColor: IWidget,
lightIntensity: IWidget,
@@ -1262,6 +1138,22 @@ function configureLoad3D(
modelWidget.callback = onModelWidgetUpdate
load3d.toggleGrid(showGrid.value as boolean)
showGrid.callback = (value: boolean) => {
load3d.toggleGrid(value)
}
load3d.toggleCamera(cameraType.value as 'perspective' | 'orthographic')
cameraType.callback = (value: 'perspective' | 'orthographic') => {
load3d.toggleCamera(value)
}
view.callback = (value: 'front' | 'top' | 'right' | 'isometric') => {
load3d.setViewPosition(value)
}
material.callback = (value: 'original' | 'normal' | 'wireframe') => {
load3d.setMaterialMode(value)
}
@@ -1420,6 +1312,14 @@ app.registerExtension({
(w: IWidget) => w.name === 'model_file'
)
const showGrid = node.widgets.find((w: IWidget) => w.name === 'show_grid')
const cameraType = node.widgets.find(
(w: IWidget) => w.name === 'camera_type'
)
const view = node.widgets.find((w: IWidget) => w.name === 'view')
const material = node.widgets.find((w: IWidget) => w.name === 'material')
const bgColor = node.widgets.find((w: IWidget) => w.name === 'bg_color')
@@ -1453,6 +1353,9 @@ app.registerExtension({
load3d,
'input',
modelWidget,
showGrid,
cameraType,
view,
material,
bgColor,
lightIntensity,
@@ -1666,6 +1569,14 @@ app.registerExtension({
(w: IWidget) => w.name === 'model_file'
)
const showGrid = node.widgets.find((w: IWidget) => w.name === 'show_grid')
const cameraType = node.widgets.find(
(w: IWidget) => w.name === 'camera_type'
)
const view = node.widgets.find((w: IWidget) => w.name === 'view')
const material = node.widgets.find((w: IWidget) => w.name === 'material')
const bgColor = node.widgets.find((w: IWidget) => w.name === 'bg_color')
@@ -1710,6 +1621,9 @@ app.registerExtension({
load3d,
'input',
modelWidget,
showGrid,
cameraType,
view,
material,
bgColor,
lightIntensity,
@@ -1738,8 +1652,6 @@ app.registerExtension({
sceneWidget.serializeValue = async () => {
node.properties['Camera Info'] = JSON.stringify(load3d.getCameraState())
load3d.toggleAnimation(false)
const { scene: imageData, mask: maskData } = await load3d.captureScene(
w.value,
h.value
@@ -1846,6 +1758,14 @@ app.registerExtension({
(w: IWidget) => w.name === 'model_file'
)
const showGrid = node.widgets.find((w: IWidget) => w.name === 'show_grid')
const cameraType = node.widgets.find(
(w: IWidget) => w.name === 'camera_type'
)
const view = node.widgets.find((w: IWidget) => w.name === 'view')
const material = node.widgets.find((w: IWidget) => w.name === 'material')
const bgColor = node.widgets.find((w: IWidget) => w.name === 'bg_color')
@@ -1881,6 +1801,9 @@ app.registerExtension({
load3d,
'output',
modelWidget,
showGrid,
cameraType,
view,
material,
bgColor,
lightIntensity,

View File

@@ -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
@@ -1155,7 +1125,7 @@ class MaskEditorDialog extends ComfyDialog {
.then((response) => {
if (!response.ok) {
console.log('Failed to upload mask:', response)
this.uploadMask(filepath, formData, retries - 1)
this.uploadMask(filepath, formData, 2)
}
})
.catch((error) => {
@@ -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)

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