Compare commits

...

80 Commits

Author SHA1 Message Date
christian-byrne
c1f30b96f9 Add key function param in node tree build 2025-01-14 18:19:15 -07:00
Gremlation
c13190cd07 Fix execution_interrupted (#2244) 2025-01-14 11:46:42 -05:00
filtered
00f031e382 [Refactor] Remove old workarounds (#2242) 2025-01-14 10:52:34 -05:00
filtered
04153caaf5 Fix prettier output in CI does not match IDE (#2243) 2025-01-14 10:51:32 -05:00
Chenlei Hu
210bfdeb7d 1.7.10 (#2241) 2025-01-13 20:25:36 -05:00
bymyself
ce0726d85e Restore all open workflows on load (#2238) 2025-01-13 20:24:40 -05:00
bymyself
dd69f9dc30 [Style] Update workflow template cards style (#2239) 2025-01-13 20:21:03 -05:00
Chenlei Hu
3f261f0e53 [Desktop] Add user journey events to the metrics collection list (UI) (#2237)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-12 17:16:16 -05:00
Chenlei Hu
3b2cc23f65 [Desktop] Mark window style setting as experimental (#2236) 2025-01-12 16:14:51 -05:00
bymyself
c50a86b258 [CI] Fix vite config condition (#2235) 2025-01-12 16:09:46 -05:00
Chenlei Hu
1a8c2bba42 1.7.9 (#2234) 2025-01-12 13:43:05 -05:00
Benjamin Lu
fc09951b3e [Style] Visual improvements to WorkflowTabs (#2232)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-01-12 13:41:55 -05:00
Terry Jia
76d5f39607 [3d] use threejs native viewHelper (#2230) 2025-01-12 13:23:23 -05:00
bymyself
9d3bc0f173 Add optional report feature to error dialog (#2229)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-12 13:23:02 -05:00
bymyself
d9b350e159 Add bookmark option in workflow tab context menu (#2231)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-12 13:22:22 -05:00
Chenlei Hu
44610674ee [Desktop] Fix server start view layout (#2226) 2025-01-10 22:14:01 -05:00
Chenlei Hu
9bfce5b8d0 Disable i18n on PRs from forked repo (#2225) 2025-01-10 19:23:20 -05:00
Chenlei Hu
8986fa356a 1.7.8 (#2223) 2025-01-10 19:06:03 -05:00
Chenlei Hu
0c4fd4af1c [Style] Set topbar height to 2.5rem (40px) (#2224) 2025-01-10 19:05:44 -05:00
Chenlei Hu
30cd46ce1f [Desktop] Allow dragging window on empty titlebar (#2222) 2025-01-10 13:41:26 -05:00
filtered
3122c33310 Allow Ctrl + C to send interrupt in terminal (#2221) 2025-01-10 13:30:11 -05:00
Chenlei Hu
91d8d04dc6 [Style] Reduce opacity on workflow tabs scrollbar (#2220) 2025-01-10 10:59:58 -05:00
Chenlei Hu
8f5aa1ff08 [Desktop] Native window in graph view (#2216) 2025-01-09 23:00:28 -05:00
Chenlei Hu
e076783b89 [Desktop] Native window virtual menu bar (#2215) 2025-01-09 20:21:03 -05:00
Chenlei Hu
04c23001fc [Desktop] Set window action buttons style (#2214) 2025-01-09 19:35:22 -05:00
Chenlei Hu
cb265fb0bf 1.7.7 (#2213) 2025-01-09 19:03:43 -05:00
Chenlei Hu
e9211fe377 [Desktop] Add window style setting (#2212)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-09 17:33:57 -05:00
Chenlei Hu
ffc7febeac [BrowserTest] Disable flaky test (#2211) 2025-01-09 14:46:09 -05:00
Chenlei Hu
b15e626607 [Style] Replace px with rem in sidebar width (#2210) 2025-01-09 14:25:34 -05:00
Chenlei Hu
906b5e35a3 [Style] Set fixed top menu bar height (#2209) 2025-01-09 14:14:14 -05:00
Chenlei Hu
e8cd9c7642 [BrowserTest] Replace ComfyPage.reload with ComfyPage.setup (#2208) 2025-01-09 12:54:05 -05:00
Chenlei Hu
93e184e379 Explicitly add zod-to-json-schema as dev dependency (#2207) 2025-01-09 11:41:40 -05:00
Chenlei Hu
1d02cd3c47 [Docs] Extract comfy workflow zod schema as json schema (#2206) 2025-01-09 11:30:47 -05:00
Chenlei Hu
b86e3f71cb 1.7.6 (#2202) 2025-01-08 11:24:59 -05:00
bymyself
1ece2462bd Use more conventional import/export icons in color palette settings (#2199)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-08 10:58:21 -05:00
bymyself
73ecacfa2d Fix searchbox dismissed when closing filter panel (#2196) 2025-01-08 10:57:55 -05:00
bymyself
67e6df7c72 Fix search result category text on light theme (#2198) 2025-01-08 10:57:10 -05:00
Jarvis
7e8510028d fix: Make sure use the correct graph parameter (#2200) 2025-01-08 10:56:27 -05:00
bymyself
dd4dd8b68a [Doc] Add anchors to sections in README (#2197) 2025-01-08 10:55:59 -05:00
bymyself
7111022617 Fix node text highlight color on light theme (#2195) 2025-01-08 09:16:57 -05:00
Chenlei Hu
daee073045 [Style] Fix second row workflow tabs overflow (#2194) 2025-01-07 16:28:49 -05:00
Chenlei Hu
a1a834a76d Support 2nd row location for workflow tabs (#2193)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-07 16:05:37 -05:00
Chenlei Hu
c437d32691 Truncate sidebar title first to avoid tool buttons wrap (#2192) 2025-01-07 15:14:09 -05:00
Chenlei Hu
0130d41be5 Try fix workflow tabs vertical scrollbar (#2191) 2025-01-07 13:39:14 -05:00
Chenlei Hu
527561d148 1.7.5 (#2189) 2025-01-07 10:47:34 -05:00
Chenlei Hu
1c4481c342 Horizontal scroll workflow tabs on overflow (#2188) 2025-01-07 10:45:58 -05:00
Chenlei Hu
07000a23d4 Wrap each init task with error handling (#2187) 2025-01-07 10:40:32 -05:00
Chenlei Hu
ea6c9e7ca5 Replace unset unknown keybinding error with warn (#2186) 2025-01-07 10:33:23 -05:00
Chenlei Hu
90698fced6 [BugFix] Migrate deprecated Comfy.Keybinding.UnsetBindings values (#2185) 2025-01-07 10:23:25 -05:00
iola1999
9716aea10d fix: Fix infinite recursion in mask upload retry logic (#2181) 2025-01-07 16:19:13 +01:00
Chenlei Hu
077ded2cce [Refactor] Simplify keybindingStore with _.groupBy (#2180) 2025-01-06 19:46:20 -05:00
Chenlei Hu
ffb20b8789 1.7.4 (#2179) 2025-01-06 19:24:40 -05:00
Chenlei Hu
549a2fdc92 Remove main_repo_release.py (#2147) 2025-01-06 17:01:41 -05:00
bymyself
e123295423 Fix saved keybinding persistence (#2176) 2025-01-06 15:54:35 -05:00
Chenlei Hu
613b44610a 1.7.3 (#2175) 2025-01-06 11:04:54 -05:00
Chenlei Hu
e82d795ff9 Trigger node def locale update manually (#2170) 2025-01-06 10:54:59 -05:00
bymyself
ba31f8fa68 Add Close Workflow to commands (#2171)
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Chenlei Hu <huchenlei@proton.me>
2025-01-06 10:50:44 -05:00
Alexander Piskun
f2eb4e1519 (fix): added missing slash to "loadWorkflow" templates endpoint (#2174) 2025-01-06 10:49:07 -05:00
Chenlei Hu
975c2248c5 Rename Keybinding.targetSelector to targetElementId (#2169)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-05 16:03:38 -05:00
bymyself
477f4b275d Fix node bookmark color customization (#2168) 2025-01-05 13:05:09 -05:00
bymyself
179b8c22a9 [BrowserTest] Refactor test fixtures (#2165) 2025-01-05 13:03:07 -05:00
bymyself
6525389273 Update Settings schema (#2167) 2025-01-05 12:56:29 -05:00
Chenlei Hu
59fc5ac77e Update litegraph 0.8.60 (#2164) 2025-01-04 20:46:38 -05:00
bymyself
ed844e04b8 Fix text wrapping in dropdown menu (#2162) 2025-01-04 20:30:58 -05:00
bymyself
972af1977d Improve menu accessibility (#2163) 2025-01-04 20:30:27 -05:00
Chenlei Hu
5ac0b7b181 1.7.2 (#2161) 2025-01-04 19:42:15 -05:00
Chenlei Hu
31ea39e44c Prevent reference sharing on settingStore content (#2160) 2025-01-04 19:31:32 -05:00
Chenlei Hu
e65653c107 Add support for custom light color palette (#2156)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-04 18:53:47 -05:00
Chenlei Hu
e46706777c [BrowserTest] Disable flaky queue sidebar tests (#2158) 2025-01-04 16:33:36 -05:00
Chenlei Hu
1a817a48cb 1.7.1 (#2152) 2025-01-03 20:19:11 -05:00
Chenlei Hu
871967349f 1.7.0 (#2151)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-03 20:14:51 -05:00
filtered
11258f4a95 Revert markdown note node, reland as new node (#2148) 2025-01-03 16:05:23 -05:00
bymyself
c3e05c2a10 Add alt text to images in gallery and queue (#2136) 2025-01-03 13:49:06 -05:00
Chenlei Hu
ea1e776dcc Update litegraph 0.8.59 (#2140) 2025-01-03 11:15:16 -05:00
Dr.Lt.Data
f0c273f845 elaborate korean translation (#2137) 2025-01-03 11:14:56 -05:00
Chenlei Hu
ea489851ed [BugFix] Fix open workflow after insert (#2138) 2025-01-03 11:09:28 -05:00
Chenlei Hu
5717c33a0b [BrowserTest] Add test on paste with link (#2130)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-02 20:38:36 -05:00
Chenlei Hu
c31919c418 Remove invertMenuScrolling extension (#2131)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-02 20:19:00 -05:00
bymyself
2e7de4701e Reduce debounce delay in settings search (#2126) 2025-01-02 19:51:31 -05:00
Chenlei Hu
f53723da0f 1.6.15 (#2129) 2025-01-02 17:54:31 -05:00
123 changed files with 3110 additions and 1015 deletions

47
.github/workflows/i18n-node-defs.yaml vendored Normal file
View File

@@ -0,0 +1,47 @@
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

View File

@@ -5,6 +5,8 @@ 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
@@ -17,13 +19,12 @@ jobs:
run: npm run dev:electron &
working-directory: ComfyUI_frontend
- name: Update en.json
run: npm run collect-i18n
run: npm run collect-i18n -- scripts/collect-i18n-general.ts
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
working-directory: ComfyUI_frontend
- name: Update translations
# Pipe output so that it doesn't error out on stdout.clearLine
run: npm run locale 2>&1 | cat
run: npm run locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
working-directory: ComfyUI_frontend

View File

@@ -25,6 +25,8 @@ 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

5
.gitignore vendored
View File

@@ -41,4 +41,7 @@ browser_tests/*/*-win32.png
dist.zip
/temp/
/temp/
# Generated JSON Schemas
/schemas/

View File

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

View File

@@ -58,7 +58,7 @@ Stable releases are published bi-weekly in the ComfyUI main repository.
### Major features
<details>
<details id='feature-native-translation'>
<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>
<details id='feature-mask-editor'>
<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>
<details id='feature-integrated-server-terminal'>
<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>
<details id='feature-keybinding-customization'>
<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>
<details id='feature-node-library-sidebar'>
<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>
<details id='feature-queue-sidebar'>
<summary>v1.2.0: Queue/History sidebar tab</summary>
https://github.com/user-attachments/assets/86e264fe-4d26-4f07-aa9a-83bdd2d02b8f
</details>
<details>
<details id='feature-node-search'>
<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>
<details id='feature-nested-group'>
<summary>v1.3.32: **Litegraph** Nested group</summary>
https://github.com/user-attachments/assets/f51adeb1-028e-40af-81e4-0ac13075198a
</details>
<details>
<details id='feature-group-selection'>
<summary>v1.3.24: **Litegraph** Group selection</summary>
https://github.com/user-attachments/assets/e6230a94-411e-4fba-90cb-6c694200adaa
</details>
<details>
<details id='feature-toggle-link-visibility'>
<summary>v1.3.6: **Litegraph** Toggle link visibility</summary>
[rec.webm](https://github.com/user-attachments/assets/34e460ac-fbbc-44ef-bfbb-99a84c2ae2be)
</details>
<details>
<details id='feature-auto-widget-conversion'>
<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>
<details id='feature-pan-mode'>
<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>
<details id='feature-shift-drag-link-creation'>
<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>
<details id='feature-optional-input-donuts'>
<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>
<details id='feature-group-title-edit'>
<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>
<details id='feature-group-selection-shortcut'>
<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>
<details id='feature-node-title-edit'>
<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>
<details id='feature-drag-multi-link'>
<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>
<details id='feature-auto-connect-link'>
<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>
<details id='feature-hide-text-overflow'>
<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>
<details id='extension-api-bottom-panel-tabs'>
<summary>v1.3.22: Register bottom panel tabs</summary>
```js
@@ -321,7 +321,7 @@ app.registerExtension({
</details>
<details>
<details id='extension-api-settings'>
<summary>v1.3.22: New settings API</summary>
Legacy settings API.
@@ -367,7 +367,7 @@ app.extensionManager.setting.set('TestSetting', 'Hello, universe!')
</details>
<details>
<details id='extension-api-commands-keybindings'>
<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>
<details id='extension-api-topbar-menu'>
<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>
<details id='extension-api-toast'>
<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>
<details id='extension-api-sidebar-tab'>
<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,10 +626,3 @@ 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,8 +1,9 @@
import { expect } from '@playwright/test'
import type { Palette } from '../src/types/colorPaletteTypes'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
const customColorPalettes = {
const customColorPalettes: Record<string, Palette> = {
obsidian: {
version: 102,
id: 'obsidian',
@@ -128,6 +129,19 @@ const customColorPalettes = {
'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'
}
}
}
}
@@ -136,12 +150,18 @@ 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.reload()
await comfyPage.setup()
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.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View File

@@ -11,6 +11,14 @@ 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.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

@@ -1,5 +1,6 @@
import { expect } from '@playwright/test'
import { Keybinding } from '../src/types/keyBindingTypes'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Load workflow warning', () => {
@@ -43,6 +44,18 @@ 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', () => {
@@ -103,4 +116,52 @@ test.describe('Settings', () => {
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed)
})
})
test('Should persist keybinding setting', async ({ comfyPage }) => {
// Open the settings dialog
await comfyPage.page.keyboard.press('Control+,')
await comfyPage.page.waitForSelector('.settings-container')
// Open the keybinding tab
await comfyPage.page.getByLabel('Keybinding').click()
await comfyPage.page.waitForSelector(
'[placeholder="Search Keybindings..."]'
)
// Focus the 'New Blank Workflow' row
const newBlankWorkflowRow = comfyPage.page.locator('tr', {
has: comfyPage.page.getByRole('cell', { name: 'New Blank Workflow' })
})
await newBlankWorkflowRow.click()
// Click edit button
const editKeybindingButton = newBlankWorkflowRow.locator('.pi-pencil')
await editKeybindingButton.click()
// Set new keybinding
const input = comfyPage.page.getByPlaceholder('Press keys for new binding')
await input.press('Alt+n')
const requestPromise = comfyPage.page.waitForRequest(
'**/api/settings/Comfy.Keybinding.NewBindings'
)
// Save keybinding
const saveButton = comfyPage.page
.getByLabel('Comfy.NewBlankWorkflow')
.getByLabel('Save')
await saveButton.click()
const request = await requestPromise
const expectedSetting: Keybinding = {
commandId: 'Comfy.NewBlankWorkflow',
combo: {
key: 'n',
ctrl: false,
alt: true,
shift: false
}
}
expect(request.postData()).toContain(JSON.stringify(expectedSetting))
})
})

View File

@@ -27,6 +27,11 @@ 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
@@ -40,19 +45,23 @@ class ComfyMenu {
}
get nodeLibraryTab() {
return new NodeLibrarySidebarTab(this.page)
this._nodeLibraryTab ??= new NodeLibrarySidebarTab(this.page)
return this._nodeLibraryTab
}
get workflowsTab() {
return new WorkflowsSidebarTab(this.page)
this._workflowsTab ??= new WorkflowsSidebarTab(this.page)
return this._workflowsTab
}
get queueTab() {
return new QueueSidebarTab(this.page)
this._queueTab ??= new QueueSidebarTab(this.page)
return this._queueTab
}
get topbar() {
return new Topbar(this.page)
this._topbar ??= new Topbar(this.page)
return this._topbar
}
async toggleTheme() {
@@ -360,11 +369,6 @@ 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)
}
@@ -821,6 +825,11 @@ export class ComfyPage {
async getNodeRefById(id: NodeId) {
return new NodeReference(id, this)
}
async getNodes() {
return await this.page.evaluate(() => {
return window['app'].graph.nodes
})
}
async getNodeRefsByType(type: string): Promise<NodeReference[]> {
return Promise.all(
(

View File

@@ -3,6 +3,13 @@ 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

@@ -152,6 +152,13 @@ export class WorkflowsSidebarTab extends SidebarTab {
await this.page.keyboard.press('Enter')
await this.page.waitForTimeout(300)
}
async insertWorkflow(locator: Locator) {
await locator.click({ button: 'right' })
await this.page
.locator('.p-contextmenu-item-content', { hasText: 'Insert' })
.click()
}
}
export class QueueSidebarTab extends SidebarTab {

View File

@@ -9,6 +9,12 @@ 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

@@ -469,6 +469,43 @@ test.describe('Canvas Interaction', () => {
expect(await getCursorStyle()).toBe('default')
})
// https://github.com/Comfy-Org/litegraph.js/pull/424
test('Properly resets dragging state after pan mode sequence', async ({
comfyPage
}) => {
const getCursorStyle = async () => {
return await comfyPage.page.evaluate(() => {
return (
document.getElementById('graph-canvas')!.style.cursor || 'default'
)
})
}
// Initial state check
await comfyPage.page.mouse.move(10, 10)
expect(await getCursorStyle()).toBe('default')
// Click and hold
await comfyPage.page.mouse.down()
expect(await getCursorStyle()).toBe('grabbing')
// Press space while holding click
await comfyPage.page.keyboard.down('Space')
expect(await getCursorStyle()).toBe('grabbing')
// Release click while space is still down
await comfyPage.page.mouse.up()
expect(await getCursorStyle()).toBe('grab')
// Release space
await comfyPage.page.keyboard.up('Space')
expect(await getCursorStyle()).toBe('default')
// Move mouse - cursor should remain default
await comfyPage.page.mouse.move(20, 20)
expect(await getCursorStyle()).toBe('default')
})
test('Can pan when dragging a link', async ({ comfyPage }) => {
const posSlot1 = comfyPage.clipTextEncodeNode1InputSlot
await comfyPage.page.mouse.move(posSlot1.x, posSlot1.y)
@@ -556,7 +593,7 @@ test.describe('Load workflow', () => {
}) => {
await comfyPage.loadWorkflow('single_ksampler')
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
await comfyPage.reload({ clearStorage: false })
await comfyPage.setup({ clearStorage: false })
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
})
@@ -573,11 +610,72 @@ test.describe('Load workflow', () => {
await expect(comfyPage.canvas).toHaveScreenshot(
'single_ksampler_modified.png'
)
await comfyPage.reload({ clearStorage: false })
await comfyPage.setup({ 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

@@ -21,7 +21,7 @@ test.describe('Menu', () => {
expect(await comfyPage.menu.getThemeId()).toBe('light')
// Theme id should persist after reload.
await comfyPage.reload()
await comfyPage.setup()
expect(await comfyPage.menu.getThemeId()).toBe('light')
await comfyPage.menu.toggleTheme()
@@ -292,6 +292,47 @@ 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', {
@@ -429,6 +470,26 @@ test.describe('Menu', () => {
])
})
test('Can open workflow after insert', async ({ comfyPage }) => {
await comfyPage.setupWorkflowsDirectory({
'workflow1.json': 'single_ksampler.json'
})
await comfyPage.setup()
const tab = comfyPage.menu.workflowsTab
await tab.open()
await comfyPage.executeCommand('Comfy.LoadDefaultWorkflow')
const originalNodeCount = (await comfyPage.getNodes()).length
await tab.insertWorkflow(tab.getPersistedItem('workflow1.json'))
await comfyPage.nextFrame()
expect((await comfyPage.getNodes()).length).toEqual(originalNodeCount + 1)
await tab.getPersistedItem('workflow1.json').click()
await comfyPage.nextFrame()
expect((await comfyPage.getNodes()).length).toEqual(1)
})
test('Can rename nested workflow from opened workflow item', async ({
comfyPage
}) => {
@@ -508,7 +569,7 @@ test.describe('Menu', () => {
})
await comfyPage.setSetting('Comfy.Locale', 'zh')
await comfyPage.reload()
await comfyPage.setup()
const downloadedContentZh = await comfyPage.getExportedWorkflow({
api: false
@@ -604,6 +665,15 @@ 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)
@@ -738,7 +808,7 @@ test.describe('Menu', () => {
})
})
test.describe('Queue sidebar', () => {
test.describe.skip('Queue sidebar', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})

View File

@@ -132,6 +132,48 @@ 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 +1,3 @@
declare const __COMFYUI_FRONTEND_VERSION__: string
declare const __SENTRY_ENABLED__: boolean
declare const __SENTRY_DSN__: string

129
package-lock.json generated
View File

@@ -1,18 +1,19 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.6.14",
"version": "1.7.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@comfyorg/comfyui-frontend",
"version": "1.6.14",
"version": "1.7.10",
"license": "GPL-3.0-only",
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.3",
"@comfyorg/litegraph": "^0.8.58",
"@comfyorg/comfyui-electron-types": "^0.4.7",
"@comfyorg/litegraph": "^0.8.60",
"@primevue/themes": "^4.0.5",
"@sentry/vue": "^8.48.0",
"@tiptap/core": "^2.10.4",
"@tiptap/extension-link": "^2.10.4",
"@tiptap/extension-table": "^2.10.4",
@@ -87,7 +88,8 @@
"vite-plugin-static-copy": "^1.0.5",
"vitest": "^2.0.5",
"vue-tsc": "^2.1.10",
"zip-dir": "^2.0.0"
"zip-dir": "^2.0.0",
"zod-to-json-schema": "^3.24.1"
}
},
"../litegraph.js": {
@@ -1934,15 +1936,15 @@
"dev": true
},
"node_modules/@comfyorg/comfyui-electron-types": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.3.tgz",
"integrity": "sha512-hSM3mchpsYN0e7oZ7XLWjEvFDvE1rgzaB9YkCeqIiZYZgLL78T79ssM0n5ra17Zv7Mqwl6ErZblXvbQE/36RPw==",
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.7.tgz",
"integrity": "sha512-APC3C4VZOo9W6h0xiAGxnsU9iNp3T8rN9w/5KmOCI0GUoKtKg5U2OaicTmnMwcDSQe5Jxflmej53GyJ1nH9oRw==",
"license": "GPL-3.0-only"
},
"node_modules/@comfyorg/litegraph": {
"version": "0.8.58",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.58.tgz",
"integrity": "sha512-V/4yC8i5QOpDV20ZiEMiZP6KnmYD5d15El3V4tmH/MkhjOxjc6owAFMyAVgpxphYdcBF2qj1QTNTrZLgC6x2VQ==",
"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": {
@@ -4461,6 +4463,96 @@
"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",
@@ -19440,21 +19532,22 @@
}
},
"node_modules/zod": {
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-to-json-schema": {
"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==",
"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==",
"dev": true,
"license": "ISC",
"peerDependencies": {
"zod": "^3.23.3"
"zod": "^3.24.1"
}
},
"node_modules/zod-validation-error": {

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.6.14",
"version": "1.7.10",
"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}' --plugin @trivago/prettier-plugin-sort-imports",
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}' --plugin @trivago/prettier-plugin-sort-imports",
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}'",
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
"test:jest": "jest --config jest.config.ts",
"test:generate": "npx tsx tests-ui/setup",
"test:browser": "npx playwright test",
@@ -28,7 +28,8 @@
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"locale": "lobe-i18n locale",
"collect-i18n": "playwright test --config=playwright.i18n.config.ts"
"collect-i18n": "playwright test --config=playwright.i18n.config.ts",
"json-schema": "tsx scripts/generate-json-schema.ts"
},
"devDependencies": {
"@babel/core": "^7.24.7",
@@ -77,13 +78,15 @@
"vite-plugin-static-copy": "^1.0.5",
"vitest": "^2.0.5",
"vue-tsc": "^2.1.10",
"zip-dir": "^2.0.0"
"zip-dir": "^2.0.0",
"zod-to-json-schema": "^3.24.1"
},
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.3",
"@comfyorg/litegraph": "^0.8.58",
"@comfyorg/comfyui-electron-types": "^0.4.7",
"@comfyorg/litegraph": "^0.8.60",
"@primevue/themes": "^4.0.5",
"@sentry/vue": "^8.48.0",
"@tiptap/core": "^2.10.4",
"@tiptap/extension-link": "^2.10.4",
"@tiptap/extension-table": "^2.10.4",

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

@@ -3,14 +3,11 @@ 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'
@@ -22,7 +19,7 @@ const extractMenuCommandLocaleStrings = (): Set<string> => {
return labels
}
test('collect-i18n', async ({ comfyPage }) => {
test('collect-i18n-general', async ({ comfyPage }) => {
const commands = (
await comfyPage.page.evaluate(() => {
const workspace = window['app'].extensionManager
@@ -129,105 +126,6 @@ test('collect-i18n', 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(
@@ -241,16 +139,13 @@ test('collect-i18n', async ({ comfyPage }) => {
...allSettingCategoriesLocale
},
serverConfigItems: allServerConfigsLocale,
serverConfigCategories: allServerConfigCategoriesLocale,
dataTypes: allDataTypesLocale,
nodeCategories: allNodeCategoriesLocale
serverConfigCategories: allServerConfigCategoriesLocale
},
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

@@ -0,0 +1,125 @@
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

@@ -0,0 +1,35 @@
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

@@ -1,71 +0,0 @@
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

@@ -10,6 +10,7 @@
--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;
@@ -763,3 +764,17 @@ 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,6 +27,7 @@
"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,6 +1,7 @@
{
"id": "light",
"name": "Light",
"light_theme": true,
"colors": {
"node_slot": {
"CLIP": "#FFA726",
@@ -13,7 +14,12 @@
"MASK": "#9CCC65",
"MODEL": "#7E57C2",
"STYLE_MODEL": "#D4E157",
"VAE": "#FF7043"
"VAE": "#FF7043",
"NOISE": "#B0B0B0",
"GUIDER": "#66FFFF",
"SAMPLER": "#ECB4B4",
"SIGMAS": "#CDFFCD",
"TAESD": "#DCC274"
},
"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==",
@@ -22,6 +28,7 @@
"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,16 +1,22 @@
<template>
<Button
<div
v-show="workspaceState.focusMode"
class="comfy-menu-hamburger"
class="comfy-menu-hamburger no-drag"
:style="positionCSS"
icon="pi pi-bars"
severity="secondary"
text
size="large"
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
@click="exitFocusMode"
@contextmenu="showNativeMenu"
/>
>
<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>
</template>
<script setup lang="ts">
@@ -51,8 +57,6 @@ const positionCSS = computed<CSSProperties>(() =>
<style scoped>
.comfy-menu-hamburger {
pointer-events: auto;
position: fixed;
z-index: 9999;
@apply pointer-events-auto fixed z-[9999] flex flex-row;
}
</style>

View File

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

View File

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

View File

@@ -0,0 +1,40 @@
<template>
<div :class="['flex flex-wrap', $attrs.class]">
<div
v-for="checkbox in checkboxes"
:key="checkbox.value"
class="flex items-center gap-2"
>
<Checkbox
v-model="internalSelection"
:inputId="checkbox.value"
:value="checkbox.value"
/>
<label :for="checkbox.value" class="ml-2">{{ checkbox.label }}</label>
</div>
</div>
</template>
<script setup lang="ts">
import Checkbox from 'primevue/checkbox'
import { computed } from 'vue'
interface CheckboxItem {
label: string
value: string
}
const props = defineProps<{
checkboxes: CheckboxItem[]
modelValue: string[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string[]): void
}>()
const internalSelection = computed({
get: () => props.modelValue,
set: (value: string[]) => emit('update:modelValue', value)
})
</script>

View File

@@ -12,12 +12,14 @@
: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">
@@ -34,9 +36,11 @@ const props = withDefaults(
src: string
class?: string | string[] | object
contain: boolean
alt?: string
}>(),
{
contain: false
contain: false,
alt: 'Image content'
}
)

View File

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

View File

@@ -5,12 +5,20 @@
:message="props.error.exception_message"
/>
<div class="comfy-error-report">
<Button
v-show="!reportOpen"
:label="$t('g.showReport')"
@click="showReport"
text
/>
<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>
<template v-if="reportOpen">
<Divider />
<ScrollPanel style="width: 100%; height: 400px; max-width: 80vw">
@@ -18,9 +26,12 @@
</ScrollPanel>
<Divider />
</template>
<ReportIssuePanel
v-if="sendReportOpen"
error-type="graphExecutionError"
:extra-fields="[stackTraceField]"
/>
<div class="action-container">
<ReportIssueButton v-if="showSendError" :error="props.error" />
<FindIssueButton
:errorMessage="props.error.exception_message"
:repoOwner="repoOwner"
@@ -41,16 +52,18 @@ import Button from 'primevue/button'
import Divider from 'primevue/divider'
import ScrollPanel from 'primevue/scrollpanel'
import { useToast } from 'primevue/usetoast'
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
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 { isElectron } from '@/utils/envUtil'
import type { ReportField } from '@/types/issueReportTypes'
import ReportIssuePanel from './error/ReportIssuePanel.vue'
const props = defineProps<{
error: ExecutionErrorWsMessage
@@ -63,9 +76,24 @@ const reportOpen = ref(false)
const showReport = () => {
reportOpen.value = true
}
const showSendError = isElectron()
const sendReportOpen = ref(false)
const showSendReport = () => {
sendReportOpen.value = true
}
const toast = useToast()
const { t } = useI18n()
const stackTraceField = computed<ReportField>(() => {
return {
label: t('issueReport.stackTrace'),
value: 'StackTrace',
optIn: true,
data: {
nodeType: props.error.node_type,
stackTrace: props.error.traceback?.join('\n')
}
}
})
onMounted(async () => {
try {

View File

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

View File

@@ -1,52 +0,0 @@
<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

@@ -0,0 +1,197 @@
<template>
<Panel>
<template #header>
<div class="flex items-center gap-2">
<span class="font-bold">{{ $t('issueReport.submitErrorReport') }}</span>
</div>
</template>
<template #footer>
<div class="flex justify-end">
<Button
v-tooltip="$t('g.reportIssueTooltip')"
:label="submitted ? $t('g.reportSent') : $t('g.reportIssue')"
:severity="isButtonDisabled ? 'secondary' : 'primary'"
:icon="icon"
:disabled="isButtonDisabled"
@click="reportIssue"
/>
</div>
</template>
<div class="p-4 mt-4 border border-round surface-border shadow-1">
<CheckboxGroup
v-model="selection"
class="gap-4 mb-4"
:checkboxes="reportCheckboxes"
/>
<div class="mb-4">
<InputText
v-model="contactInfo"
class="w-full"
:placeholder="$t('issueReport.provideEmail')"
:maxlength="CONTACT_MAX_LEN"
/>
<CheckboxGroup
v-model="contactPrefs"
class="gap-3 mt-2"
:checkboxes="contactCheckboxes"
/>
</div>
<div class="mb-4">
<Textarea
v-model="details"
class="w-full"
rows="4"
:maxlength="DETAILS_MAX_LEN"
:placeholder="$t('issueReport.provideAdditionalDetails')"
:aria-label="$t('issueReport.provideAdditionalDetails')"
/>
</div>
</div>
</Panel>
</template>
<script setup lang="ts">
import type { CaptureContext, User } from '@sentry/core'
import { captureMessage } from '@sentry/core'
import cloneDeep from 'lodash/cloneDeep'
import Button from 'primevue/button'
import InputText from 'primevue/inputtext'
import Panel from 'primevue/panel'
import Textarea from 'primevue/textarea'
import { useToast } from 'primevue/usetoast'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import CheckboxGroup from '@/components/common/CheckboxGroup.vue'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import type { DefaultField, ReportField } from '@/types/issueReportTypes'
const ISSUE_NAME = 'User reported issue'
const DETAILS_MAX_LEN = 5_000
const CONTACT_MAX_LEN = 320
const props = defineProps<{
errorType: string
defaultFields?: DefaultField[]
extraFields?: ReportField[]
}>()
const { defaultFields = ['Workflow', 'Logs', 'SystemStats', 'Settings'] } =
props
const { t } = useI18n()
const toast = useToast()
const selection = ref<string[]>([])
const contactPrefs = ref<string[]>([])
const contactInfo = ref('')
const details = ref('')
const submitting = ref(false)
const submitted = ref(false)
const followUp = computed(() => contactPrefs.value.includes('FollowUp'))
const notifyResolve = computed(() => contactPrefs.value.includes('Resolution'))
const icon = computed(() => {
if (submitting.value) return 'pi pi-spin pi-spinner'
if (submitted.value) return 'pi pi-check'
return 'pi pi-send'
})
const isFormEmpty = computed(() => !selection.value.length && !details.value)
const isButtonDisabled = computed(
() => submitted.value || submitting.value || isFormEmpty.value
)
const contactCheckboxes = [
{ label: t('issueReport.contactFollowUp'), value: 'FollowUp' },
{ label: t('issueReport.notifyResolve'), value: 'Resolution' }
]
const defaultReportCheckboxes = [
{ label: t('g.workflow'), value: 'Workflow' },
{ label: t('g.logs'), value: 'Logs' },
{ label: t('issueReport.systemStats'), value: 'SystemStats' },
{ label: t('g.settings'), value: 'Settings' }
]
const reportCheckboxes = computed(() => [
...(props.extraFields?.map(({ label, value }) => ({ label, value })) ?? []),
...defaultReportCheckboxes.filter(({ value }) =>
defaultFields.includes(value as DefaultField)
)
])
const getUserInfo = (): User => ({ email: contactInfo.value })
const getLogs = async () =>
selection.value.includes('Logs') ? api.getLogs() : null
const getSystemStats = async () =>
selection.value.includes('SystemStats') ? api.getSystemStats() : null
const getSettings = async () =>
selection.value.includes('Settings') ? api.getSettings() : null
const getWorkflow = () =>
selection.value.includes('Workflow')
? cloneDeep(app.graph.asSerialisable())
: null
const createDefaultFields = async () => {
const [settings, systemStats, logs, workflow] = await Promise.all([
getSettings(),
getSystemStats(),
getLogs(),
getWorkflow()
])
return { settings, systemStats, logs, workflow }
}
const createExtraFields = (): Record<string, unknown> | undefined => {
if (!props.extraFields) return undefined
return props.extraFields
.filter((field) => !field.optIn || selection.value.includes(field.value))
.reduce((acc, field) => ({ ...acc, ...cloneDeep(field.data) }), {})
}
const createFeedback = () => {
return {
details: details.value,
contactPreferences: {
followUp: followUp.value,
notifyOnResolution: notifyResolve.value
}
}
}
const createCaptureContext = async (): Promise<CaptureContext> => {
return {
user: getUserInfo(),
level: 'error',
tags: {
errorType: props.errorType
},
extra: {
...createFeedback(),
...(await createDefaultFields()),
...createExtraFields()
}
}
}
const reportIssue = async () => {
if (isButtonDisabled.value) return
submitting.value = true
try {
captureMessage(ISSUE_NAME, await createCaptureContext())
submitted.value = true
toast.add({
severity: 'success',
summary: t('g.reportSent'),
life: 3000
})
} finally {
submitting.value = false
}
}
</script>

View File

@@ -0,0 +1,230 @@
// @ts-strict-ignore
import { createTestingPinia } from '@pinia/testing'
import { mount } from '@vue/test-utils'
import Button from 'primevue/button'
import PrimeVue from 'primevue/config'
import InputText from 'primevue/inputtext'
import Panel from 'primevue/panel'
import Textarea from 'primevue/textarea'
import Tooltip from 'primevue/tooltip'
import { beforeAll, describe, expect, it, vi } from 'vitest'
import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'
import CheckboxGroup from '@/components/common/CheckboxGroup.vue'
import enMesages from '@/locales/en/main.json'
import { DefaultField, ReportField } from '@/types/issueReportTypes'
import ReportIssuePanel from '../ReportIssuePanel.vue'
type ReportIssuePanelProps = {
errorType: string
defaultFields?: DefaultField[]
extraFields?: ReportField[]
}
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()
}))
describe('ReportIssuePanel', () => {
beforeAll(() => {
const app = createApp({})
app.use(PrimeVue)
})
const mountComponent = (props: ReportIssuePanelProps, options = {}): any => {
return mount(ReportIssuePanel, {
global: {
plugins: [PrimeVue, createTestingPinia(), i18n],
directives: { tooltip: Tooltip },
components: { InputText, Button, Panel, Textarea, CheckboxGroup }
},
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(CheckboxGroup).length).toBe(2)
expect(wrapper.findComponent(InputText).exists()).toBe(true)
expect(wrapper.findComponent(Textarea).exists()).toBe(true)
expect(wrapper.findComponent(Button).exists()).toBe(true)
})
it('updates selection when checkboxes are selected', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
await checkboxes?.setValue(['Workflow', 'Logs'])
expect(wrapper.vm.selection).toEqual(['Workflow', 'Logs'])
})
it('updates contactInfo when input is changed', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const input = wrapper.findComponent(InputText)
await input.setValue('test@example.com')
expect(wrapper.vm.contactInfo).toBe('test@example.com')
})
it('updates additional details when textarea is changed', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const textarea = wrapper.findComponent(Textarea)
await textarea.setValue('This is a test detail.')
expect(wrapper.vm.details).toBe('This is a test detail.')
})
it('updates contactPrefs when preferences are selected', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const preferences = wrapper.findAllComponents(CheckboxGroup).at(1)
await preferences?.setValue(['FollowUp'])
expect(wrapper.vm.contactPrefs).toEqual(['FollowUp'])
})
it('does not allow submission if the form is empty', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
await wrapper.vm.reportIssue()
expect(wrapper.vm.submitted).toBe(false)
})
it('renders with overridden default fields', () => {
const wrapper = mountComponent({
errorType: 'Test Error',
defaultFields: ['Settings']
})
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
expect(checkboxes?.props('checkboxes')).toEqual([
{ label: 'Settings', value: 'Settings' }
])
})
it('renders additional fields when extraFields prop is provided', () => {
const extraFields = [
{ label: 'Custom Field', value: 'CustomField', optIn: true, data: {} }
]
const wrapper = mountComponent({ errorType: 'Test Error', extraFields })
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
expect(checkboxes?.props('checkboxes')).toContainEqual({
label: 'Custom Field',
value: 'CustomField'
})
})
it('does not submit unchecked fields', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const textarea = wrapper.findComponent(Textarea)
await textarea.setValue('Report with only text but no fields selected')
await wrapper.vm.reportIssue()
const { captureMessage } = (await import('@sentry/core')) as any
const captureContext = captureMessage.mock.calls[0][1]
expect(captureContext.extra.logs).toBeNull()
expect(captureContext.extra.systemStats).toBeNull()
expect(captureContext.extra.settings).toBeNull()
expect(captureContext.extra.workflow).toBeNull()
})
it.each([
{
checkbox: 'Logs',
apiMethod: 'getLogs',
expectedKey: 'logs',
mockValue: 'mock logs'
},
{
checkbox: 'SystemStats',
apiMethod: 'getSystemStats',
expectedKey: 'systemStats',
mockValue: 'mock stats'
},
{
checkbox: 'Settings',
apiMethod: 'getSettings',
expectedKey: 'settings',
mockValue: 'mock settings'
}
])(
'submits (%s) when the (%s) checkbox is selected',
async ({ checkbox, apiMethod, expectedKey, mockValue }) => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const { api } = (await import('@/scripts/api')) as any
vi.spyOn(api, apiMethod).mockResolvedValue(mockValue)
const { captureMessage } = await import('@sentry/core')
// Select the checkbox
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
await checkboxes?.vm.$emit('update:modelValue', [checkbox])
await wrapper.vm.reportIssue()
expect(api[apiMethod]).toHaveBeenCalled()
// Verify the message includes the associated data
expect(captureMessage).toHaveBeenCalledWith(
'User reported issue',
expect.objectContaining({
extra: expect.objectContaining({ [expectedKey]: mockValue })
})
)
}
)
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 { captureMessage } = await import('@sentry/core')
const mockWorkflow = { nodes: [], edges: [] }
vi.spyOn(app.graph, 'asSerialisable').mockReturnValue(mockWorkflow)
// Select the "Workflow" checkbox
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
await checkboxes?.vm.$emit('update:modelValue', ['Workflow'])
await wrapper.vm.reportIssue()
expect(app.graph.asSerialisable).toHaveBeenCalled()
// Verify the message includes the workflow
expect(captureMessage).toHaveBeenCalledWith(
'User reported issue',
expect.objectContaining({
extra: expect.objectContaining({ workflow: mockWorkflow })
})
)
})
})

View File

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

View File

@@ -13,6 +13,9 @@
<BottomPanel />
</template>
<template #graph-canvas-panel>
<SecondRowWorkflowTabs
v-if="workflowTabsPosition === 'Topbar (2nd-row)'"
/>
<GraphCanvasMenu v-if="canvasMenuEnabled" />
</template>
</LiteGraphCanvasSplitterOverlay>
@@ -48,12 +51,13 @@ 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 { setStorageValue } from '@/scripts/utils'
import { getStorageValue, setStorageValue } from '@/scripts/utils'
import { IS_CONTROL_WIDGET, updateControlWidgetLabel } from '@/scripts/widgets'
import { useColorPaletteService } from '@/services/colorPaletteService'
import { useLitegraphService } from '@/services/litegraphService'
@@ -83,11 +87,37 @@ 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) {
@@ -357,6 +387,18 @@ 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,6 +6,7 @@
severity="secondary"
icon="pi pi-plus"
v-tooltip.left="t('graphCanvasMenu.zoomIn')"
:aria-label="$t('graphCanvasMenu.zoomIn')"
@mousedown="repeat('Comfy.Canvas.ZoomIn')"
@mouseup="stopRepeat"
/>
@@ -13,6 +14,7 @@
severity="secondary"
icon="pi pi-minus"
v-tooltip.left="t('graphCanvasMenu.zoomOut')"
:aria-label="$t('graphCanvasMenu.zoomOut')"
@mousedown="repeat('Comfy.Canvas.ZoomOut')"
@mouseup="stopRepeat"
/>
@@ -20,6 +22,7 @@
severity="secondary"
icon="pi pi-expand"
v-tooltip.left="t('graphCanvasMenu.fitView')"
:aria-label="$t('graphCanvasMenu.fitView')"
@click="() => commandStore.execute('Comfy.Canvas.FitView')"
/>
<Button
@@ -30,6 +33,12 @@
(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>
@@ -43,6 +52,7 @@
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,10 +59,27 @@
</h4>
<ul class="list-disc pl-6 space-y-1">
<li>
{{ $t('install.settings.dataCollectionDialog.errorReports') }}
{{
$t('install.settings.dataCollectionDialog.collect.errorReports')
}}
</li>
<li>
{{ $t('install.settings.dataCollectionDialog.systemInfo') }}
{{ $t('install.settings.dataCollectionDialog.collect.systemInfo') }}
</li>
<li>
{{
$t(
'install.settings.dataCollectionDialog.collect.userJourneyEvents'
)
}}
<span
class="pi pi-info-circle text-neutral-400"
v-tooltip="
$t(
'install.settings.dataCollectionDialog.collect.userJourneyTooltip'
)
"
/>
</li>
</ul>
@@ -72,21 +89,29 @@
<ul class="list-disc pl-6 space-y-1">
<li>
{{
$t('install.settings.dataCollectionDialog.personalInformation')
}}
</li>
<li>
{{ $t('install.settings.dataCollectionDialog.workflowContents') }}
</li>
<li>
{{
$t('install.settings.dataCollectionDialog.fileSystemInformation')
$t(
'install.settings.dataCollectionDialog.doNotCollect.personalInformation'
)
}}
</li>
<li>
{{
$t(
'install.settings.dataCollectionDialog.customNodeConfigurations'
'install.settings.dataCollectionDialog.doNotCollect.workflowContents'
)
}}
</li>
<li>
{{
$t(
'install.settings.dataCollectionDialog.doNotCollect.fileSystemInformation'
)
}}
</li>
<li>
{{
$t(
'install.settings.dataCollectionDialog.doNotCollect.customNodeConfigurations'
)
}}
</li>

View File

@@ -130,12 +130,13 @@
</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 { TorchDeviceType, electronAPI } from '@/utils/envUtil'
import { electronAPI } from '@/utils/envUtil'
const { t } = useI18n()

View File

@@ -19,7 +19,13 @@
class="filter-button z-10"
@click="nodeSearchFilterVisible = true"
/>
<Dialog v-model:visible="nodeSearchFilterVisible" class="min-w-96">
<Dialog
v-model:visible="nodeSearchFilterVisible"
class="min-w-96"
dismissable-mask
modal
@hide="reFocusInput"
>
<template #header>
<h3>Add node filter condition</h3>
</template>
@@ -140,7 +146,6 @@ 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-gray-400 overflow-hidden text-ellipsis whitespace-nowrap"
class="option-category font-light text-sm text-muted overflow-hidden text-ellipsis whitespace-nowrap"
>
{{ nodeDef.category.replaceAll('/', ' > ') }}
</div>

View File

@@ -71,11 +71,11 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
<style>
:root {
--sidebar-width: 64px;
--sidebar-width: 4rem;
--sidebar-icon-size: 1.5rem;
}
:root .small-sidebar {
--sidebar-width: 40px;
--sidebar-width: 2.5rem;
--sidebar-icon-size: 1rem;
}
</style>

View File

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

View File

@@ -20,6 +20,14 @@
@click="alphabeticalSort = !alphabeticalSort"
v-tooltip.bottom="$t('sideToolbar.nodeLibraryTab.sortOrder')"
/>
<Button
class="grouping-button"
:icon="groupBySource ? 'pi pi-list' : 'pi pi-list-check'"
text
severity="secondary"
@click="groupBySource = !groupBySource"
v-tooltip.bottom="$t('sideToolbar.nodeLibraryTab.groupingType')"
/>
</template>
<template #header>
<SearchBox
@@ -66,7 +74,7 @@ import Button from 'primevue/button'
import Divider from 'primevue/divider'
import Popover from 'primevue/popover'
import type { TreeNode } from 'primevue/treenode'
import { Ref, computed, nextTick, ref } from 'vue'
import { Ref, computed, nextTick, ref, watch } from 'vue'
import SearchBox from '@/components/common/SearchBox.vue'
import { SearchFilter } from '@/components/common/SearchFilterChip.vue'
@@ -101,6 +109,21 @@ const nodeBookmarkTreeExplorerRef = ref<InstanceType<
> | null>(null)
const searchFilter = ref(null)
const alphabeticalSort = ref(false)
const groupBySource = ref(false)
const createSourceKey = (nodeDef: ComfyNodeDefImpl) => {
const sourcePath = nodeDef.python_module.split('.')
const pathWithoutCategory = nodeDef.nodePath.split('/').slice(1)
return [...sourcePath, ...pathWithoutCategory]
}
watch(groupBySource, (newValue) => {
if (newValue) {
nodeDefStore.setKeyFunction(createSourceKey)
} else {
nodeDefStore.setKeyFunction(null)
}
})
const searchQuery = ref<string>('')
@@ -194,4 +217,9 @@ const onRemoveFilter = (filterAndValue) => {
}
handleSearch(searchQuery.value)
}
// This can be added if the persistent state is not desirable:
// onBeforeUnmount(() => {
// nodeDefStore.setKeyFunction(null)
// })
</script>

View File

@@ -4,11 +4,11 @@
:class="props.class"
>
<div class="comfy-vue-side-bar-header">
<Toolbar
class="flex-shrink-0 border-x-0 border-t-0 rounded-none px-2 py-1 min-h-8"
>
<Toolbar class="border-x-0 border-t-0 rounded-none px-2 py-1 min-h-8">
<template #start>
<span class="text-sm">{{ props.title.toUpperCase() }}</span>
<span class="text-xs 2xl:text-sm truncate" :title="props.title">
{{ props.title.toUpperCase() }}
</span>
</template>
<template #end>
<slot name="tool-buttons"></slot>
@@ -37,4 +37,8 @@ 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,6 +24,7 @@
:key="item.url"
:src="item.url"
:contain="false"
:alt="item.filename"
class="galleria-image"
v-if="item.isImage"
/>

View File

@@ -5,6 +5,7 @@
: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-lg cursor-pointer w-64 h-64"
class="relative overflow-hidden rounded-t-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-lg object-cover thumbnail"
class="w-64 h-64 rounded-t-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)">
<div @click="loadWorkflow(slotProps.data)" class="p-2">
<TemplateWorkflowCard
:moduleName="selectedTab.moduleName"
:workflowName="slotProps.data"
@@ -112,7 +112,7 @@ const loadWorkflow = async (id: string) => {
let json
if (selectedTab.value.moduleName === 'default') {
// Default templates provided by frontend are served on this separate endpoint
json = await fetch(api.fileURL(`templates/${id}.json`)).then((r) =>
json = await fetch(api.fileURL(`/templates/${id}.json`)).then((r) =>
r.json()
)
} else {

View File

@@ -3,6 +3,7 @@
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 p-1 keybinding-tag"
class="ml-auto border border-surface rounded text-muted text-xs text-nowrap p-1 keybinding-tag"
>
{{ item.comfyCommand.keybinding.combo.toString() }}
</span>

View File

@@ -0,0 +1,15 @@
<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,34 +3,44 @@
<div
ref="topMenuRef"
class="comfyui-menu flex items-center"
v-show="betaMenuEnabled && !workspaceState.focusMode"
v-show="showTopMenu"
:class="{ dropzone: isDropZone, 'dropzone-active': isDroppable }"
>
<h1 class="comfyui-logo mx-2">ComfyUI</h1>
<h1 class="comfyui-logo mx-2 app-drag">ComfyUI</h1>
<CommandMenubar />
<Divider layout="vertical" class="mx-2" />
<div class="flex-grow">
<div class="flex-grow min-w-0 app-drag h-full">
<WorkflowTabs v-if="workflowTabsPosition === 'Topbar'" />
</div>
<div class="comfyui-menu-right" ref="menuRight"></div>
<Actionbar />
<BottomPanelToggleButton />
<BottomPanelToggleButton class="flex-shrink-0" />
<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'
@@ -40,21 +50,27 @@ import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
import { app } from '@/scripts/app'
import { useSettingStore } from '@/stores/settingStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { showNativeMenu } from '@/utils/envUtil'
import { electronAPI, isElectron, showNativeMenu } from '@/utils/envUtil'
const workspaceState = useWorkspaceStore()
const settingStore = useSettingStore()
const workflowTabsPosition = computed(() =>
settingStore.get('Comfy.Workflow.WorkflowTabsPosition')
)
const betaMenuEnabled = computed(
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
)
const menuSetting = computed(() => settingStore.get('Comfy.UseNewMenu'))
const betaMenuEnabled = computed(() => menuSetting.value !== '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
@@ -75,11 +91,20 @@ 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);
@@ -89,7 +114,6 @@ eventBus.on((event: string, payload: any) => {
z-index: 1000;
order: 0;
grid-column: 1/-1;
max-height: 90vh;
}
.comfyui-menu.dropzone {

View File

@@ -1,34 +1,48 @@
<template>
<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" />
<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>
</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'
@@ -36,7 +50,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 } from '@/stores/workflowStore'
import { ComfyWorkflow, useWorkflowBookmarkStore } from '@/stores/workflowStore'
import { useWorkflowStore } from '@/stores/workflowStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
@@ -53,6 +67,7 @@ const { t } = useI18n()
const workspaceStore = useWorkspaceStore()
const workflowStore = useWorkflowStore()
const workflowService = useWorkflowService()
const workflowBookmarkStore = useWorkflowBookmarkStore()
const rightClickedTab = ref<WorkflowOption>(null)
const menu = ref()
@@ -140,26 +155,56 @@ 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) {
@apply p-0 bg-transparent rounded-none flex-shrink-0 relative;
: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.p-togglebutton-checked) {
@apply border-b-2;
@apply border-b border-solid h-full;
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;
@@ -172,4 +217,18 @@ const commandStore = useCommandStore()
: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 } from '@/types/colorPaletteTypes'
import type { ColorPalettes, CompletedPalette } from '@/types/colorPaletteTypes'
export const CORE_COLOR_PALETTES: ColorPalettes = {
dark,
@@ -15,4 +15,6 @@ export const CORE_COLOR_PALETTES: ColorPalettes = {
github
} as const
export const DEFAULT_COLOR_PALETTE = dark
export const DEFAULT_COLOR_PALETTE: CompletedPalette = dark
export const DEFAULT_DARK_COLOR_PALETTE: CompletedPalette = dark
export const DEFAULT_LIGHT_COLOR_PALETTE: CompletedPalette = light

View File

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

View File

@@ -85,7 +85,8 @@ export const CORE_SETTINGS: SettingParams[] = [
name: 'Sidebar size',
type: 'combo',
options: ['normal', 'small'],
defaultValue: () => (window.innerWidth < 1600 ? 'small' : 'normal')
// Default to small if the window is less than 1536px(2xl) wide.
defaultValue: () => (window.innerWidth < 1536 ? 'small' : 'normal')
},
{
id: 'Comfy.TextareaWidget.FontSize',
@@ -399,8 +400,10 @@ export const CORE_SETTINGS: SettingParams[] = [
id: 'Comfy.Workflow.WorkflowTabsPosition',
name: 'Opened workflows position',
type: 'combo',
options: ['Sidebar', 'Topbar'],
defaultValue: 'Topbar'
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'
},
{
id: 'Comfy.Graph.CanvasMenu',
@@ -423,7 +426,16 @@ export const CORE_SETTINGS: SettingParams[] = [
name: 'Keybindings unset by the user',
type: 'hidden',
defaultValue: [] as Keybinding[],
versionAdded: '1.3.7'
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
})
}
},
{
id: 'Comfy.Keybinding.NewBindings',

View File

@@ -30,10 +30,28 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
{
id: 'Comfy-Desktop.SendStatistics',
category: ['Comfy-Desktop', 'General', 'Send Statistics'],
name: 'Send anonymous crash reports',
name: 'Send anonymous usage metrics',
type: 'boolean',
defaultValue: true,
onChange: onChangeRestartApp
},
{
id: 'Comfy-Desktop.WindowStyle',
category: ['Comfy-Desktop', 'General', 'Window Style'],
name: 'Window Style',
tooltip: 'Choose custom option to hide the system title bar',
type: 'combo',
experimental: true,
defaultValue: 'default',
options: ['default', 'custom'],
onChange: (
newValue: 'default' | 'custom',
oldValue: 'default' | 'custom'
) => {
electronAPI.Config.setWindowStyle(newValue)
onChangeRestartApp(newValue, oldValue)
}
}
],

View File

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

View File

@@ -1,41 +0,0 @@
// @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,6 +2,7 @@
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'
@@ -115,8 +116,7 @@ class Load3d {
stlLoader: STLLoader
currentModel: THREE.Object3D | null = null
originalModel: THREE.Object3D | THREE.BufferGeometry | GLTF | null = null
node: any
private animationFrameId: number | null = null
animationFrameId: number | null = null
gridHelper: THREE.GridHelper
lights: THREE.Light[] = []
clock: THREE.Clock
@@ -131,6 +131,10 @@ 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()
@@ -157,6 +161,7 @@ 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
@@ -203,13 +208,143 @@ class Load3d {
this.standardMaterial = this.createSTLMaterial()
this.animate()
this.createViewHelper(container)
this.createGridSwitcher(container)
this.createCameraSwitcher(container)
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
@@ -465,6 +600,13 @@ 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()
}
@@ -501,8 +643,16 @@ 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()
}
@@ -588,6 +738,7 @@ class Load3d {
}
this.controls.dispose()
this.viewHelper.dispose()
this.renderer.dispose()
this.renderer.domElement.remove()
this.scene.clear()
@@ -818,10 +969,12 @@ 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')
@@ -846,44 +999,6 @@ 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)
@@ -1020,16 +1135,28 @@ class Load3dAnimation extends Load3d {
})
}
animate = () => {
requestAnimationFrame(this.animate)
if (this.currentAnimation && this.isAnimationPlaying) {
startAnimation() {
const animate = () => {
this.animationFrameId = requestAnimationFrame(animate)
const delta = this.clock.getDelta()
this.currentAnimation.update(delta)
}
this.controls.update()
this.renderer.render(this.scene, this.activeCamera)
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)
}
animate()
}
}
@@ -1076,9 +1203,6 @@ function configureLoad3D(
load3d: Load3d,
loadFolder: 'input' | 'output',
modelWidget: IWidget,
showGrid: IWidget,
cameraType: IWidget,
view: IWidget,
material: IWidget,
bgColor: IWidget,
lightIntensity: IWidget,
@@ -1138,22 +1262,6 @@ 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)
}
@@ -1312,14 +1420,6 @@ 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')
@@ -1353,9 +1453,6 @@ app.registerExtension({
load3d,
'input',
modelWidget,
showGrid,
cameraType,
view,
material,
bgColor,
lightIntensity,
@@ -1569,14 +1666,6 @@ 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')
@@ -1621,9 +1710,6 @@ app.registerExtension({
load3d,
'input',
modelWidget,
showGrid,
cameraType,
view,
material,
bgColor,
lightIntensity,
@@ -1652,6 +1738,8 @@ 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
@@ -1758,14 +1846,6 @@ 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')
@@ -1801,9 +1881,6 @@ app.registerExtension({
load3d,
'output',
modelWidget,
showGrid,
cameraType,
view,
material,
bgColor,
lightIntensity,

View File

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

View File

@@ -25,11 +25,11 @@ app.registerExtension({
if (!this.properties) {
this.properties = { text: '' }
}
ComfyWidgets.MARKDOWN(
ComfyWidgets.STRING(
// Should we extends LGraphNode? Yesss
this,
'',
['', { default: this.properties.text }],
['', { default: this.properties.text, multiline: true }],
app
)
@@ -50,5 +50,33 @@ app.registerExtension({
)
NoteNode.category = 'utils'
/** Markdown variant of NoteNode */
class MarkdownNoteNode extends LGraphNode {
static title = 'Markdown Note'
color = LGraphCanvas.node_colors.yellow.color
bgcolor = LGraphCanvas.node_colors.yellow.bgcolor
groupcolor = LGraphCanvas.node_colors.yellow.groupcolor
constructor(title?: string) {
super(title)
if (!this.properties) {
this.properties = { text: '' }
}
ComfyWidgets.MARKDOWN(
this,
'',
['', { default: this.properties.text }],
app
)
this.serialize_widgets = true
this.isVirtualNode = true
}
}
LiteGraph.registerNodeType('MarkdownNote', MarkdownNoteNode)
MarkdownNoteNode.category = 'utils'
}
})

View File

@@ -14,11 +14,14 @@ export function useTerminal(element: Ref<HTMLElement>) {
terminal.loadAddon(fitAddon)
terminal.attachCustomKeyEventHandler((event) => {
if (event.type === 'keydown' && (event.ctrlKey || event.metaKey)) {
if (event.key === 'c' || event.key === 'v') {
// Allow default browser copy/paste handling
return false
}
// Allow default browser copy/paste handling
if (
event.type === 'keydown' &&
(event.ctrlKey || event.metaKey) &&
((event.key === 'c' && terminal.hasSelection()) || event.key === 'v')
) {
// TODO: Deselect text after copy/paste; use IPC.
return false
}
return true
})

View File

@@ -5,6 +5,10 @@ import {
LiteGraph
} from '@comfyorg/litegraph'
import {
DEFAULT_DARK_COLOR_PALETTE,
DEFAULT_LIGHT_COLOR_PALETTE
} from '@/constants/coreColorPalettes'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
@@ -16,6 +20,7 @@ import { useSettingStore } from '@/stores/settingStore'
import { useToastStore } from '@/stores/toastStore'
import { type ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
@@ -23,6 +28,7 @@ export function useCoreCommands(): ComfyCommand[] {
const workflowService = useWorkflowService()
const workflowStore = useWorkflowStore()
const dialogService = useDialogService()
const colorPaletteStore = useColorPaletteStore()
const getTracker = () => workflowStore.activeWorkflow?.changeTracker
const getSelectedNodes = (): LGraphNode[] => {
@@ -410,18 +416,18 @@ export function useCoreCommands(): ComfyCommand[] {
label: 'Toggle Theme (Dark/Light)',
versionAdded: '1.3.12',
function: (() => {
let previousDarkTheme: string = 'dark'
let previousDarkTheme: string = DEFAULT_DARK_COLOR_PALETTE.id
let previousLightTheme: string = DEFAULT_LIGHT_COLOR_PALETTE.id
// Official light theme is the only light theme supported now.
const isDarkMode = (themeId: string) => themeId !== 'light'
return () => {
const settingStore = useSettingStore()
const currentTheme = settingStore.get('Comfy.ColorPalette')
if (isDarkMode(currentTheme)) {
previousDarkTheme = currentTheme
settingStore.set('Comfy.ColorPalette', 'light')
} else {
const theme = colorPaletteStore.completedActivePalette
if (theme.light_theme) {
previousLightTheme = theme.id
settingStore.set('Comfy.ColorPalette', previousDarkTheme)
} else {
previousDarkTheme = theme.id
settingStore.set('Comfy.ColorPalette', previousLightTheme)
}
}
})()
@@ -522,6 +528,16 @@ export function useCoreCommands(): ComfyCommand[] {
function: () => {
workflowService.duplicateWorkflow(workflowStore.activeWorkflow!)
}
},
{
id: 'Workspace.CloseWorkflow',
icon: 'pi pi-times',
label: 'Close Current Workflow',
versionAdded: '1.7.3',
function: () => {
if (workflowStore.activeWorkflow)
workflowService.closeWorkflow(workflowStore.activeWorkflow)
}
}
]
}

View File

@@ -152,6 +152,9 @@
"Comfy_Undo": {
"label": "Undo"
},
"Workspace_CloseWorkflow": {
"label": "Close Current Workflow"
},
"Workspace_NextOpenedWorkflow": {
"label": "Next Opened Workflow"
},

View File

@@ -68,7 +68,19 @@
"disableAll": "Disable All",
"command": "Command",
"keybinding": "Keybinding",
"upload": "Upload"
"upload": "Upload",
"export": "Export",
"workflow": "Workflow"
},
"issueReport": {
"submitErrorReport": "Submit Error Report (Optional)",
"provideEmail": "Give us your email (Optional)",
"provideAdditionalDetails": "Provide additional details (optional)",
"stackTrace": "Stack Trace",
"systemStats": "System Stats",
"contactFollowUp": "Contact me for follow up",
"notifyResolve": "Notify me when resolved",
"helpFix": "Help Fix This"
},
"color": {
"default": "Default",
@@ -169,21 +181,26 @@
},
"settings": {
"autoUpdate": "Automatic Updates",
"allowMetrics": "Crash Reports",
"autoUpdateDescription": "Automatically download and install updates when they become available. You'll always be notified before updates are installed.",
"allowMetricsDescription": "Help improve ComfyUI by sending anonymous crash reports. No personal information or workflow content will be collected. This can be disabled at any time in the settings menu.",
"allowMetrics": "Usage Metrics",
"autoUpdateDescription": "Automatically download updates when they become available. You will be notified before updates are installed.",
"allowMetricsDescription": "Help improve ComfyUI by sending anonymous usage metrics. No personal information or workflow content will be collected.",
"learnMoreAboutData": "Learn more about data collection",
"dataCollectionDialog": {
"title": "About Data Collection",
"whatWeCollect": "What we collect:",
"whatWeDoNotCollect": "What we don't collect:",
"errorReports": "Error message and stack trace",
"systemInfo": "Hardware, OS type, and app version",
"personalInformation": "Personal information",
"workflowContent": "Workflow content",
"fileSystemInformation": "File system information",
"workflowContents": "Workflow contents",
"customNodeConfigurations": "Custom node configurations"
"collect": {
"errorReports": "Error message and stack trace",
"systemInfo": "Hardware, OS type, and app version",
"userJourneyEvents": "User journey events",
"userJourneyTooltip": "User journey events are used to track the user's journey through the app installation process. The event collection ends on the first successful ComfyUI workflow run."
},
"doNotCollect": {
"personalInformation": "Personal information",
"fileSystemInformation": "File system information",
"workflowContents": "Workflow contents",
"customNodeConfigurations": "Custom node configurations"
}
}
},
"customNodes": "Custom Nodes",
@@ -217,7 +234,8 @@
"openWorkflow": "Open workflow in local file system",
"newBlankWorkflow": "Create a new blank workflow",
"nodeLibraryTab": {
"sortOrder": "Sort Order"
"sortOrder": "Sort Order",
"groupingType": "Grouping Type"
},
"modelLibrary": "Model Library",
"downloads": "Downloads",
@@ -278,7 +296,9 @@
"closeTab": "Close Tab",
"closeTabsToLeft": "Close Tabs to Left",
"closeTabsToRight": "Close Tabs to Right",
"closeOtherTabs": "Close Other Tabs"
"closeOtherTabs": "Close Other Tabs",
"addToBookmarks": "Add to Bookmarks",
"removeFromBookmarks": "Remove from Bookmarks"
},
"templateWorkflows": {
"title": "Get Started with a Template",
@@ -374,6 +394,7 @@
"Show Settings Dialog": "Show Settings Dialog",
"Toggle Theme (Dark/Light)": "Toggle Theme (Dark/Light)",
"Undo": "Undo",
"Close Current Workflow": "Close Current Workflow",
"Next Opened Workflow": "Next Opened Workflow",
"Previous Opened Workflow": "Previous Opened Workflow",
"Toggle Search Box": "Toggle Search Box",

View File

@@ -184,6 +184,9 @@
},
"type": {
"name": "type"
},
"device": {
"name": "device"
}
}
},
@@ -1100,6 +1103,15 @@
}
}
},
"DevToolsNodeWithSeedInput": {
"display_name": "Node With Seed Input",
"description": "A node with a seed input",
"inputs": {
"seed": {
"name": "seed"
}
}
},
"DevToolsNodeWithStringInput": {
"display_name": "Node With String Input",
"description": "A node with a string input",
@@ -1124,6 +1136,21 @@
}
}
},
"DevToolsObjectPatchNode": {
"display_name": "Object Patch Node",
"description": "A node that applies an object patch",
"inputs": {
"model": {
"name": "model"
},
"target_module": {
"name": "target_module"
},
"dummy_float": {
"name": "dummy_float"
}
}
},
"DevToolsSimpleSlider": {
"display_name": "Simple Slider",
"inputs": {
@@ -1197,6 +1224,9 @@
},
"type": {
"name": "type"
},
"device": {
"name": "device"
}
}
},
@@ -2293,6 +2323,9 @@
},
"up_direction": {
"name": "up_direction"
},
"fov": {
"name": "fov"
}
},
"outputs": {
@@ -2345,6 +2378,9 @@
},
"animation_speed": {
"name": "animation_speed"
},
"fov": {
"name": "fov"
}
},
"outputs": {
@@ -4249,6 +4285,9 @@
},
"up_direction": {
"name": "up_direction"
},
"fov": {
"name": "fov"
}
}
},

View File

@@ -3,7 +3,15 @@
"name": "Automatically check for updates"
},
"Comfy-Desktop_SendStatistics": {
"name": "Send anonymous crash reports"
"name": "Send anonymous usage metrics"
},
"Comfy-Desktop_WindowStyle": {
"name": "Window Style",
"tooltip": "Choose custom option to hide the system title bar",
"options": {
"default": "default",
"custom": "custom"
}
},
"Comfy_ConfirmClear": {
"name": "Require confirmation when clearing workflow"
@@ -60,9 +68,6 @@
"Comfy_GroupSelectedNodes_Padding": {
"name": "Group selected nodes padding"
},
"Comfy_InvertMenuScrolling": {
"name": "Invert Context Menu Scrolling"
},
"Comfy_LinkRelease_Action": {
"name": "Action on link release (No modifier)",
"options": {
@@ -296,7 +301,8 @@
"name": "Opened workflows position",
"options": {
"Sidebar": "Sidebar",
"Topbar": "Topbar"
"Topbar": "Topbar",
"Topbar (2nd-row)": "Topbar (2nd-row)"
}
},
"LiteGraph_Canvas_MaximumFps": {

View File

@@ -152,6 +152,9 @@
"Comfy_Undo": {
"label": "Annuler"
},
"Workspace_CloseWorkflow": {
"label": "Fermer le flux de travail actuel"
},
"Workspace_NextOpenedWorkflow": {
"label": "Flux de travail ouvert suivant"
},

View File

@@ -85,6 +85,7 @@
"enableAll": "Activer tout",
"error": "Erreur",
"experimental": "BETA",
"export": "Exportation",
"extensionName": "Nom de l'extension",
"findIssues": "Trouver des problèmes",
"firstTimeUIMessage": "C'est la première fois que vous utilisez la nouvelle interface utilisateur. Choisissez \"Menu > Utiliser le nouveau menu > Désactivé\" pour restaurer l'ancienne interface utilisateur.",
@@ -131,7 +132,8 @@
"systemInfo": "Informations système",
"terminal": "Terminal",
"upload": "Téléverser",
"videoFailedToLoad": "Échec du chargement de la vidéo"
"videoFailedToLoad": "Échec du chargement de la vidéo",
"workflow": "Flux de travail"
},
"graphCanvasMenu": {
"fitView": "Adapter la vue",
@@ -204,27 +206,42 @@
"pathValidationFailed": "Échec de la validation du chemin",
"selectItemsToMigrate": "Sélectionnez les éléments à migrer",
"settings": {
"allowMetrics": "Rapports de plantage",
"allowMetricsDescription": "Aidez à améliorer ComfyUI en envoyant des rapports de plantage anonymes. Aucune information personnelle ou contenu de flux de travail ne sera collecté. Cela peut être désactivé à tout moment dans le menu des paramètres.",
"allowMetrics": "Métriques d'utilisation",
"allowMetricsDescription": "Aidez à améliorer ComfyUI en envoyant des métriques d'utilisation anonymes. Aucune information personnelle ou contenu de flux de travail ne sera collecté.",
"autoUpdate": "Mises à jour automatiques",
"autoUpdateDescription": "Téléchargez et installez automatiquement les mises à jour lorsqu'elles deviennent disponibles. Vous serez toujours informé avant l'installation des mises à jour.",
"dataCollectionDialog": {
"customNodeConfigurations": "Configurations de nœuds personnalisés",
"errorReports": "Message d'erreur et trace de la pile",
"fileSystemInformation": "Informations sur le système de fichiers",
"personalInformation": "Informations personnelles",
"systemInfo": "Matériel, type d'OS et version de l'application",
"collect": {
"errorReports": "Message d'erreur et trace de la pile",
"systemInfo": "Matériel, type de système d'exploitation et version de l'application",
"userJourneyEvents": "Événements du parcours utilisateur",
"userJourneyTooltip": "Les événements du parcours utilisateur sont utilisés pour suivre le parcours de l'utilisateur lors du processus d'installation de l'application. La collecte d'événements se termine lors de la première exécution réussie du flux de travail ComfyUI."
},
"doNotCollect": {
"customNodeConfigurations": "Configurations de nœud personnalisées",
"fileSystemInformation": "Informations sur le système de fichiers",
"personalInformation": "Informations personnelles",
"workflowContents": "Contenus du flux de travail"
},
"title": "À propos de la collecte de données",
"whatWeCollect": "Ce que nous collectons :",
"whatWeDoNotCollect": "Ce que nous ne collectons pas :",
"workflowContent": "Contenu du flux de travail",
"workflowContents": "Contenus du flux de travail"
"whatWeDoNotCollect": "Ce que nous ne collectons pas :"
},
"learnMoreAboutData": "En savoir plus sur la collecte de données"
},
"systemLocations": "Emplacements système",
"unhandledError": "Erreur inconnue"
},
"issueReport": {
"contactFollowUp": "Contactez-moi pour un suivi",
"helpFix": "Aidez à résoudre cela",
"notifyResolve": "Prévenez-moi lorsque résolu",
"provideAdditionalDetails": "Fournir des détails supplémentaires (facultatif)",
"provideEmail": "Donnez-nous votre email (Facultatif)",
"stackTrace": "Trace de la pile",
"submitErrorReport": "Soumettre un rapport d'erreur (Facultatif)",
"systemStats": "Statistiques du système"
},
"menu": {
"autoQueue": "File d'attente automatique",
"batchCount": "Nombre de lots",
@@ -257,6 +274,7 @@
"Clear Pending Tasks": "Effacer les tâches en attente",
"Clear Workflow": "Effacer le flux de travail",
"Clipspace": "Espace de clip",
"Close Current Workflow": "Fermer le flux de travail actuel",
"Collapse/Expand Selected Nodes": "Réduire/Étendre les nœuds sélectionnés",
"Comfy-Org Discord": "Discord de Comfy-Org",
"ComfyUI Docs": "Docs de ComfyUI",
@@ -621,11 +639,13 @@
"workflows": "Flux de travail"
},
"tabMenu": {
"addToBookmarks": "Ajouter aux Favoris",
"closeOtherTabs": "Fermer les autres onglets",
"closeTab": "Fermer l'onglet",
"closeTabsToLeft": "Fermer les onglets à gauche",
"closeTabsToRight": "Fermer les onglets à droite",
"duplicateTab": "Dupliquer l'onglet"
"duplicateTab": "Dupliquer l'onglet",
"removeFromBookmarks": "Retirer des Favoris"
},
"templateWorkflows": {
"template": {

View File

@@ -119,6 +119,9 @@
"clip_name": {
"name": "clip_name"
},
"device": {
"name": "appareil"
},
"type": {
"name": "type"
}
@@ -1100,6 +1103,15 @@
}
}
},
"DevToolsNodeWithSeedInput": {
"description": "Un nœud avec une entrée de graine",
"display_name": "Nœud Avec Entrée de Graine",
"inputs": {
"seed": {
"name": "graine"
}
}
},
"DevToolsNodeWithStringInput": {
"description": "Un nœud avec une entrée de chaîne",
"display_name": "Nœud avec entrée de chaîne",
@@ -1124,6 +1136,21 @@
}
}
},
"DevToolsObjectPatchNode": {
"description": "Un nœud qui applique un patch d'objet",
"display_name": "Nœud de Patch d'Objet",
"inputs": {
"dummy_float": {
"name": "flottant_factice"
},
"model": {
"name": "modèle"
},
"target_module": {
"name": "module_cible"
}
}
},
"DevToolsSimpleSlider": {
"display_name": "Curseur simple",
"inputs": {
@@ -1195,6 +1222,9 @@
"clip_name2": {
"name": "nom_clip2"
},
"device": {
"name": "appareil"
},
"type": {
"name": "type"
}
@@ -2358,6 +2388,9 @@
"camera_type": {
"name": "type_de_caméra"
},
"fov": {
"name": "fov"
},
"height": {
"name": "hauteur"
},
@@ -2410,6 +2443,9 @@
"camera_type": {
"name": "type_de_caméra"
},
"fov": {
"name": "fov"
},
"height": {
"name": "hauteur"
},
@@ -4232,6 +4268,9 @@
"camera_type": {
"name": "type_de_camera"
},
"fov": {
"name": "fov"
},
"light_intensity": {
"name": "intensité_de_lumière"
},

View File

@@ -3,7 +3,15 @@
"name": "Vérifier automatiquement les mises à jour"
},
"Comfy-Desktop_SendStatistics": {
"name": "Envoyer des rapports de plantage anonymes"
"name": "Envoyer des métriques d'utilisation anonymes"
},
"Comfy-Desktop_WindowStyle": {
"name": "Style de fenêtre",
"options": {
"custom": "personnalisé",
"default": "défaut"
},
"tooltip": "Choisissez l'option personnalisée pour masquer la barre de titre du système"
},
"Comfy_ConfirmClear": {
"name": "Demander une confirmation lors de l'effacement du flux de travail"
@@ -60,9 +68,6 @@
"Comfy_Group_DoubleClickTitleToEdit": {
"name": "Double-cliquer sur le titre du groupe pour le modifier"
},
"Comfy_InvertMenuScrolling": {
"name": "Inverser le défilement du menu contextuel"
},
"Comfy_LinkRelease_Action": {
"name": "Action lors du relâchement du lien (sans modificateur)",
"options": {
@@ -296,7 +301,8 @@
"name": "Position des flux de travail ouverts",
"options": {
"Sidebar": "Barre latérale",
"Topbar": "Barre supérieure"
"Topbar": "Barre supérieure",
"Topbar (2nd-row)": "Barre supérieure (2ème rangée)"
}
},
"LiteGraph_Canvas_MaximumFps": {

View File

@@ -152,6 +152,9 @@
"Comfy_Undo": {
"label": "元に戻す"
},
"Workspace_CloseWorkflow": {
"label": "現在のワークフローを閉じる"
},
"Workspace_NextOpenedWorkflow": {
"label": "次の開いたワークフロー"
},

View File

@@ -85,6 +85,7 @@
"enableAll": "すべて有効にする",
"error": "エラー",
"experimental": "ベータ",
"export": "エクスポート",
"extensionName": "拡張機能名",
"findIssues": "問題を見つける",
"firstTimeUIMessage": "新しいUIを初めて使用しています。「メニュー > 新しいメニューを使用 > 無効」を選択して古いUIに戻してください。",
@@ -131,7 +132,8 @@
"systemInfo": "システム情報",
"terminal": "ターミナル",
"upload": "アップロード",
"videoFailedToLoad": "ビデオの読み込みに失敗しました"
"videoFailedToLoad": "ビデオの読み込みに失敗しました",
"workflow": "ワークフロー"
},
"graphCanvasMenu": {
"fitView": "ビューに合わせる",
@@ -204,27 +206,42 @@
"pathValidationFailed": "パスの検証に失敗しました",
"selectItemsToMigrate": "移行する項目を選択",
"settings": {
"allowMetrics": "クラッシュレポート",
"allowMetricsDescription": "ComfyUI改善に協力してください。匿名のクラッシュレポートを送信します。個人情報やワークフロー内容は収集されません。この設定はいつでも無効にできます。",
"allowMetrics": "使用状況のメトリクス",
"allowMetricsDescription": "匿名の使用状況メトリクスを送信してComfyUI改善します。個人情報やワークフロー内容は収集されません。",
"autoUpdate": "自動更新",
"autoUpdateDescription": "更新が利用可能になると、自動的にダウンロードおよびインストールを行います。インストール前に通知が表示されます。",
"dataCollectionDialog": {
"customNodeConfigurations": "カスタムノード設定",
"errorReports": "エラーメッセージとスタックトレース",
"fileSystemInformation": "ファイルシステム情報",
"personalInformation": "個人情報",
"systemInfo": "ハードウェア、OSの種類、アプリのバージョン",
"collect": {
"errorReports": "エラーメッセージとスタックトレース",
"systemInfo": "ハードウェア、OSタイプ、アプリバージョン",
"userJourneyEvents": "ユーザージャーニーイベント",
"userJourneyTooltip": "ユーザージャーニーイベントは、アプリのインストールプロセスを通じてユーザーの旅を追跡するために使用されます。イベントの収集は、最初の成功したComfyUIワークフローの実行で終了します。"
},
"doNotCollect": {
"customNodeConfigurations": "カスタムノードの設定",
"fileSystemInformation": "ファイルシステム情報",
"personalInformation": "個人情報",
"workflowContents": "ワークフローの内容"
},
"title": "データ収集について",
"whatWeCollect": "収集内容:",
"whatWeDoNotCollect": "収集しない内容:",
"workflowContent": "ワークフロー内容",
"workflowContents": "ワークフロー内容"
"whatWeDoNotCollect": "収集しない内容:"
},
"learnMoreAboutData": "データ収集の詳細を見る"
},
"systemLocations": "システムの場所",
"unhandledError": "未知のエラー"
},
"issueReport": {
"contactFollowUp": "フォローアップのために私に連絡する",
"helpFix": "これを修正するのを助ける",
"notifyResolve": "解決したときに通知する",
"provideAdditionalDetails": "追加の詳細を提供する(オプション)",
"provideEmail": "あなたのメールアドレスを教えてください(オプション)",
"stackTrace": "スタックトレース",
"submitErrorReport": "エラーレポートを提出する(オプション)",
"systemStats": "システム統計"
},
"menu": {
"autoQueue": "自動キュー",
"batchCount": "バッチ数",
@@ -257,6 +274,7 @@
"Clear Pending Tasks": "保留中のタスクをクリア",
"Clear Workflow": "ワークフローをクリア",
"Clipspace": "クリップスペース",
"Close Current Workflow": "現在のワークフローを閉じる",
"Collapse/Expand Selected Nodes": "選択したノードの折りたたみ/展開",
"Comfy-Org Discord": "Comfy-Org Discord",
"ComfyUI Docs": "ComfyUIのドキュメント",
@@ -621,11 +639,13 @@
"workflows": "ワークフロー"
},
"tabMenu": {
"addToBookmarks": "ブックマークに追加",
"closeOtherTabs": "他のタブを閉じる",
"closeTab": "タブを閉じる",
"closeTabsToLeft": "左のタブを閉じる",
"closeTabsToRight": "右のタブを閉じる",
"duplicateTab": "タブを複製"
"duplicateTab": "タブを複製",
"removeFromBookmarks": "ブックマークから削除"
},
"templateWorkflows": {
"template": {

View File

@@ -119,6 +119,9 @@
"clip_name": {
"name": "clip名"
},
"device": {
"name": "デバイス"
},
"type": {
"name": "タイプ"
}
@@ -1100,6 +1103,15 @@
}
}
},
"DevToolsNodeWithSeedInput": {
"description": "シード入力付きのノード",
"display_name": "シード入力付きノード",
"inputs": {
"seed": {
"name": "シード"
}
}
},
"DevToolsNodeWithStringInput": {
"description": "文字列入力を持つノード",
"display_name": "文字列入力ノード",
@@ -1124,6 +1136,21 @@
}
}
},
"DevToolsObjectPatchNode": {
"description": "オブジェクトパッチを適用するノード",
"display_name": "オブジェクトパッチノード",
"inputs": {
"dummy_float": {
"name": "ダミーフロート"
},
"model": {
"name": "モデル"
},
"target_module": {
"name": "ターゲットモジュール"
}
}
},
"DevToolsSimpleSlider": {
"display_name": "シンプルスライダー",
"inputs": {
@@ -1195,6 +1222,9 @@
"clip_name2": {
"name": "clip_name2"
},
"device": {
"name": "デバイス"
},
"type": {
"name": "タイプ"
}
@@ -2358,6 +2388,9 @@
"camera_type": {
"name": "カメラタイプ"
},
"fov": {
"name": "fov"
},
"height": {
"name": "高さ"
},
@@ -2410,6 +2443,9 @@
"camera_type": {
"name": "カメラタイプ"
},
"fov": {
"name": "fov"
},
"height": {
"name": "高さ"
},
@@ -4232,6 +4268,9 @@
"camera_type": {
"name": "カメラタイプ"
},
"fov": {
"name": "fov"
},
"light_intensity": {
"name": "光の強度"
},

View File

@@ -3,7 +3,15 @@
"name": "自動的に更新を確認する"
},
"Comfy-Desktop_SendStatistics": {
"name": "匿名のクラッシュレポートを送信する"
"name": "匿名の使用統計を送信する"
},
"Comfy-Desktop_WindowStyle": {
"name": "ウィンドウスタイル",
"options": {
"custom": "カスタム",
"default": "デフォルト"
},
"tooltip": "システムタイトルバーを非表示にするにはカスタムオプションを選択してください"
},
"Comfy_ConfirmClear": {
"name": "ワークフローをクリアする際に確認を要求する"
@@ -60,9 +68,6 @@
"Comfy_Group_DoubleClickTitleToEdit": {
"name": "グループタイトルをダブルクリックして編集"
},
"Comfy_InvertMenuScrolling": {
"name": "コンテキストメニューのスクロールを反転する"
},
"Comfy_LinkRelease_Action": {
"name": "リンクリリース時のアクション(修飾子なし)",
"options": {
@@ -296,7 +301,8 @@
"name": "開いているワークフローの位置",
"options": {
"Sidebar": "サイドバー",
"Topbar": "トップバー"
"Topbar": "トップバー",
"Topbar (2nd-row)": "トップバー2行目"
}
},
"LiteGraph_Canvas_MaximumFps": {

View File

@@ -152,6 +152,9 @@
"Comfy_Undo": {
"label": "실행 취소"
},
"Workspace_CloseWorkflow": {
"label": "현재 워크플로우 닫기"
},
"Workspace_NextOpenedWorkflow": {
"label": "다음 열린 워크플로"
},

View File

@@ -85,6 +85,7 @@
"enableAll": "모두 활성화",
"error": "오류",
"experimental": "베타",
"export": "내보내기",
"extensionName": "확장 이름",
"findIssues": "문제 찾기",
"firstTimeUIMessage": "새 UI를 처음 사용합니다. \"메뉴 > 새 메뉴 사용 > 비활성화\"를 선택하여 이전 UI로 복원하세요.",
@@ -131,7 +132,8 @@
"systemInfo": "시스템 정보",
"terminal": "터미널",
"upload": "업로드",
"videoFailedToLoad": "비디오를 로드하지 못했습니다."
"videoFailedToLoad": "비디오를 로드하지 못했습니다.",
"workflow": "워크플로우"
},
"graphCanvasMenu": {
"fitView": "보기 맞춤",
@@ -204,27 +206,42 @@
"pathValidationFailed": "경로 유효성 검사 실패",
"selectItemsToMigrate": "마이그레이션 항목 선택",
"settings": {
"allowMetrics": "충돌 보고서",
"allowMetricsDescription": "익명의 충돌 보고서를 보내 ComfyUI 개선 도움을 줍니다. 개인 정보나 워크플로 내용은 수집되지 않습니다. 이는 설정 메뉴에서 언제든지 비활성화할 수 있습니다.",
"allowMetrics": "사용 통계",
"allowMetricsDescription": "익명의 사용 통계를 보내 ComfyUI 개선하는 데 도움을 줍니다. 개인 정보나 워크플로 내용은 수집되지 않습니다.",
"autoUpdate": "자동 업데이트",
"autoUpdateDescription": "업데이트가 가능해지면 자동으로 다운로드하고 설치합니다. 업데이트가 설치되기 전에 항상 알림을 받습니다.",
"dataCollectionDialog": {
"customNodeConfigurations": "사용자 정의 노드 설정",
"errorReports": "오류 메시지 및 스택 추적",
"fileSystemInformation": "파일 시스템 정보",
"personalInformation": "개인 정보",
"systemInfo": "하드웨어, OS 유형 및 앱 버전",
"collect": {
"errorReports": "오류 메시지 및 스택 추적",
"systemInfo": "하드웨어, OS 유형, 앱 버전",
"userJourneyEvents": "사용자 여정 이벤트",
"userJourneyTooltip": "사용자 여정 이벤트는 앱 설치 과정을 통한 사용자의 여정을 추적하는 데 사용됩니다. 이벤트 수집은 첫 번째 성공적인 ComfyUI 워크플로우 실행에서 종료됩니다."
},
"doNotCollect": {
"customNodeConfigurations": "사용자 정의 노드 구성",
"fileSystemInformation": "파일 시스템 정보",
"personalInformation": "개인 정보",
"workflowContents": "워크플로우 내용"
},
"title": "데이터 수집 안내",
"whatWeCollect": "수집하는 정보:",
"whatWeDoNotCollect": "수집하지 않는 정보:",
"workflowContent": "워크플로 내용",
"workflowContents": "워크플로 내용"
"whatWeDoNotCollect": "수집하지 않는 정보:"
},
"learnMoreAboutData": "데이터 수집에 대해 더 알아보기"
},
"systemLocations": "시스템 위치",
"unhandledError": "알 수 없는 오류"
},
"issueReport": {
"contactFollowUp": "추적 조사를 위해 연락해 주세요",
"helpFix": "이 문제 해결에 도움을 주세요",
"notifyResolve": "해결되었을 때 알려주세요",
"provideAdditionalDetails": "추가 세부 사항 제공 (선택 사항)",
"provideEmail": "이메일을 알려주세요 (선택 사항)",
"stackTrace": "스택 추적",
"submitErrorReport": "오류 보고서 제출 (선택 사항)",
"systemStats": "시스템 통계"
},
"menu": {
"autoQueue": "자동 실행 큐",
"batchCount": "배치 수",
@@ -257,6 +274,7 @@
"Clear Pending Tasks": "보류 중인 작업 제거하기",
"Clear Workflow": "워크플로 지우기",
"Clipspace": "클립스페이스",
"Close Current Workflow": "현재 워크플로우 닫기",
"Collapse/Expand Selected Nodes": "선택한 노드 축소/확장",
"Comfy-Org Discord": "Comfy-Org 디스코드",
"ComfyUI Docs": "ComfyUI 문서",
@@ -621,11 +639,13 @@
"workflows": "워크플로"
},
"tabMenu": {
"addToBookmarks": "북마크에 추가",
"closeOtherTabs": "다른 탭 닫기",
"closeTab": "탭 닫기",
"closeTabsToLeft": "왼쪽 탭 닫기",
"closeTabsToRight": "오른쪽 탭 닫기",
"duplicateTab": "탭 복제"
"duplicateTab": "탭 복제",
"removeFromBookmarks": "북마크에서 제거"
},
"templateWorkflows": {
"template": {

View File

@@ -119,6 +119,9 @@
"clip_name": {
"name": "CLIP 파일명"
},
"device": {
"name": "장치"
},
"type": {
"name": "유형"
}
@@ -1100,6 +1103,15 @@
}
}
},
"DevToolsNodeWithSeedInput": {
"description": "시드 입력이 있는 노드",
"display_name": "시드 입력이 있는 노드",
"inputs": {
"seed": {
"name": "시드"
}
}
},
"DevToolsNodeWithStringInput": {
"description": "문자열 입력이 있는 노드",
"display_name": "문자열 입력이 있는 노드",
@@ -1124,6 +1136,21 @@
}
}
},
"DevToolsObjectPatchNode": {
"description": "오브젝트 패치를 적용하는 노드",
"display_name": "오브젝트 패치 노드",
"inputs": {
"dummy_float": {
"name": "더미 플로트"
},
"model": {
"name": "모델"
},
"target_module": {
"name": "대상 모듈"
}
}
},
"DevToolsSimpleSlider": {
"display_name": "간단한 슬라이더",
"inputs": {
@@ -1195,6 +1222,9 @@
"clip_name2": {
"name": "CLIP 파일명2"
},
"device": {
"name": "장치"
},
"type": {
"name": "유형"
}
@@ -2242,7 +2272,7 @@
"name": "길이"
},
"samples": {
"name": "샘플"
"name": "잠재 데이터"
}
}
},
@@ -2358,6 +2388,9 @@
"camera_type": {
"name": "카메라 유형"
},
"fov": {
"name": "시야"
},
"height": {
"name": "높이"
},
@@ -2410,6 +2443,9 @@
"camera_type": {
"name": "카메라 유형"
},
"fov": {
"name": "시야"
},
"height": {
"name": "높이"
},
@@ -4232,6 +4268,9 @@
"camera_type": {
"name": "카메라 유형"
},
"fov": {
"name": "시야"
},
"light_intensity": {
"name": "조명 강도"
},
@@ -5253,7 +5292,7 @@
"display_name": "VAE 디코드",
"inputs": {
"samples": {
"name": "샘플",
"name": "잠재 데이터",
"tooltip": "디코딩할 잠재입니다."
},
"vae": {

View File

@@ -3,7 +3,15 @@
"name": "자동 업데이트 확인"
},
"Comfy-Desktop_SendStatistics": {
"name": "익명으로 충돌 보고서 전송"
"name": "익명 사용 통계 보내기"
},
"Comfy-Desktop_WindowStyle": {
"name": "창 스타일",
"options": {
"custom": "사용자 정의",
"default": "기본"
},
"tooltip": "시스템 제목 표시 줄을 숨기려면 사용자 정의 옵션을 선택하세요"
},
"Comfy_ConfirmClear": {
"name": "워크플로 비우기 시 확인 요구"
@@ -60,9 +68,6 @@
"Comfy_Group_DoubleClickTitleToEdit": {
"name": "그룹 제목을 두 번 클릭하여 편집"
},
"Comfy_InvertMenuScrolling": {
"name": "컨텍스트 메뉴 스크롤 반전"
},
"Comfy_LinkRelease_Action": {
"name": "링크 해제 시 동작 (수정자 없음)",
"options": {
@@ -296,7 +301,8 @@
"name": "열린 워크플로 위치",
"options": {
"Sidebar": "사이드바",
"Topbar": "상단바"
"Topbar": "상단바",
"Topbar (2nd-row)": "상단바 (2번째 행)"
}
},
"LiteGraph_Canvas_MaximumFps": {

View File

@@ -152,6 +152,9 @@
"Comfy_Undo": {
"label": "Отменить"
},
"Workspace_CloseWorkflow": {
"label": "Закрыть текущий рабочий процесс"
},
"Workspace_NextOpenedWorkflow": {
"label": "Следующий открытый рабочий процесс"
},

View File

@@ -85,6 +85,7 @@
"enableAll": "Включить все",
"error": "Ошибка",
"experimental": "БЕТА",
"export": "Экспорт",
"extensionName": "Имя расширения",
"findIssues": "Найти проблемы",
"firstTimeUIMessage": "Вы впервые используете новый интерфейс. Выберите \"Меню > Использовать новое меню > Отключено\", чтобы восстановить старый интерфейс.",
@@ -131,7 +132,8 @@
"systemInfo": "Информация о системе",
"terminal": "Терминал",
"upload": "Загрузить",
"videoFailedToLoad": "Не удалось загрузить видео"
"videoFailedToLoad": "Не удалось загрузить видео",
"workflow": "Рабочий процесс"
},
"graphCanvasMenu": {
"fitView": "Подгонять под выделенные",
@@ -204,27 +206,42 @@
"pathValidationFailed": "Не удалось проверить путь",
"selectItemsToMigrate": "Выберите элементы для миграции",
"settings": {
"allowMetrics": "Отчеты о сбоях",
"allowMetricsDescription": "Помогите улучшить ComfyUI, отправляя анонимные отчеты о сбоях. Личная информация или содержимое рабочего процесса не будут собираться. Это можно отключить в любое время в меню настроек.",
"allowMetrics": "Метрики использования",
"allowMetricsDescription": "Помогите улучшить ComfyUI, отправляя анонимные метрики использования. Личная информация или содержание рабочего процесса не будут собираться.",
"autoUpdate": "Автоматические обновления",
"autoUpdateDescription": "Автоматически загружать и устанавливать обновления, когда они становятся доступными. Вы всегда будете уведомлены перед установкой обновлений.",
"dataCollectionDialog": {
"customNodeConfigurations": "Конфигурации пользовательских узлов",
"errorReports": "Сообщения об ошибках и трассировка стека",
"fileSystemInformation": "Информация о файловой системе",
"personalInformation": "Личная информация",
"systemInfo": "Аппаратное обеспечение, тип ОС и версия приложения",
"collect": {
"errorReports": "Сообщение об ошибке и трассировка стека",
"systemInfo": "Аппаратное обеспечение, тип ОС и версия приложения",
"userJourneyEvents": "События пользовательского пути",
"userJourneyTooltip": "События пользовательского пути используются для отслеживания пути пользователя в процессе установки приложения. Сбор событий заканчивается после первого успешного запуска рабочего процесса ComfyUI."
},
"doNotCollect": {
"customNodeConfigurations": "Пользовательские конфигурации узлов",
"fileSystemInformation": "Информация о файловой системе",
"personalInformation": "Личная информация",
"workflowContents": "Содержание рабочего процесса"
},
"title": "О сборе данных",
"whatWeCollect": "Что мы собираем:",
"whatWeDoNotCollect": "Что мы не собираем:",
"workflowContent": "Содержимое рабочего процесса",
"workflowContents": "Содержимое рабочего процесса"
"whatWeDoNotCollect": "Что мы не собираем:"
},
"learnMoreAboutData": "Узнать больше о сборе данных"
},
"systemLocations": "Системные места",
"unhandledError": "Неизвестная ошибка"
},
"issueReport": {
"contactFollowUp": "Свяжитесь со мной для уточнения",
"helpFix": "Помочь исправить это",
"notifyResolve": "Уведомить меня, когда проблема будет решена",
"provideAdditionalDetails": "Предоставьте дополнительные сведения (необязательно)",
"provideEmail": "Укажите вашу электронную почту (необязательно)",
"stackTrace": "Трассировка стека",
"submitErrorReport": "Отправить отчет об ошибке (необязательно)",
"systemStats": "Статистика системы"
},
"menu": {
"autoQueue": "Автоочередь",
"batchCount": "Количество пакетов",
@@ -257,6 +274,7 @@
"Clear Pending Tasks": "Очистить ожидающие задачи",
"Clear Workflow": "Очистить рабочий процесс",
"Clipspace": "Клиппространство",
"Close Current Workflow": "Закрыть текущий рабочий процесс",
"Collapse/Expand Selected Nodes": "Свернуть/развернуть выбранные узлы",
"Comfy-Org Discord": "Discord Comfy-Org",
"ComfyUI Docs": "Документация ComfyUI",
@@ -621,11 +639,13 @@
"workflows": "Рабочие процессы"
},
"tabMenu": {
"addToBookmarks": "Добавить в закладки",
"closeOtherTabs": "Закрыть другие вкладки",
"closeTab": "Закрыть вкладку",
"closeTabsToLeft": "Закрыть вкладки слева",
"closeTabsToRight": "Закрыть вкладки справа",
"duplicateTab": "Дублировать вкладку"
"duplicateTab": "Дублировать вкладку",
"removeFromBookmarks": "Удалить из закладок"
},
"templateWorkflows": {
"template": {

View File

@@ -119,6 +119,9 @@
"clip_name": {
"name": "имя_clip"
},
"device": {
"name": "устройство"
},
"type": {
"name": "тип"
}
@@ -1100,6 +1103,15 @@
}
}
},
"DevToolsNodeWithSeedInput": {
"description": "Узел с входным сигналом seed",
"display_name": "Узел с входным сигналом Seed",
"inputs": {
"seed": {
"name": "seed"
}
}
},
"DevToolsNodeWithStringInput": {
"description": "Узел со строковым вводом",
"display_name": "Узел со строковым вводом",
@@ -1124,6 +1136,21 @@
}
}
},
"DevToolsObjectPatchNode": {
"description": "Узел, применяющий объектный патч",
"display_name": "Узел объектного патча",
"inputs": {
"dummy_float": {
"name": "фиктивное число с плавающей точкой"
},
"model": {
"name": "модель"
},
"target_module": {
"name": "целевой модуль"
}
}
},
"DevToolsSimpleSlider": {
"display_name": "Простой Слайдер",
"inputs": {
@@ -1195,6 +1222,9 @@
"clip_name2": {
"name": "clip_name2"
},
"device": {
"name": "устройство"
},
"type": {
"name": "тип"
}
@@ -2358,6 +2388,9 @@
"camera_type": {
"name": "тип_камеры"
},
"fov": {
"name": "поле зрения"
},
"height": {
"name": "высота"
},
@@ -2410,6 +2443,9 @@
"camera_type": {
"name": "тип_камеры"
},
"fov": {
"name": "поле зрения"
},
"height": {
"name": "высота"
},
@@ -4232,6 +4268,9 @@
"camera_type": {
"name": "тип_камеры"
},
"fov": {
"name": "поле зрения"
},
"light_intensity": {
"name": "интенсивность_света"
},

View File

@@ -3,7 +3,15 @@
"name": "Автоматически проверять обновления"
},
"Comfy-Desktop_SendStatistics": {
"name": "Отправлять анонимные отчеты о сбоях"
"name": "Отправлять анонимную статистику использования"
},
"Comfy-Desktop_WindowStyle": {
"name": "Стиль окна",
"options": {
"custom": "пользовательский",
"default": "по умолчанию"
},
"tooltip": "Выберите пользовательский вариант, чтобы скрыть системную строку заголовка"
},
"Comfy_ConfirmClear": {
"name": "Требовать подтверждение при очистке рабочего процесса"
@@ -60,9 +68,6 @@
"Comfy_Group_DoubleClickTitleToEdit": {
"name": "Дважды щелкните по заголовку группы, чтобы редактировать"
},
"Comfy_InvertMenuScrolling": {
"name": "Инвертировать прокрутку контекстного меню"
},
"Comfy_LinkRelease_Action": {
"name": "Действие при отпускании ссылки (без модификатора)",
"options": {
@@ -296,7 +301,8 @@
"name": "Положение открытых рабочих процессов",
"options": {
"Sidebar": "Боковая панель",
"Topbar": "Верхняя панель"
"Topbar": "Верхняя панель",
"Topbar (2nd-row)": "Топбар (2-й ряд)"
}
},
"LiteGraph_Canvas_MaximumFps": {

View File

@@ -152,6 +152,9 @@
"Comfy_Undo": {
"label": "撤销"
},
"Workspace_CloseWorkflow": {
"label": "关闭当前工作流"
},
"Workspace_NextOpenedWorkflow": {
"label": "下一个打开的工作流"
},

View File

@@ -85,6 +85,7 @@
"enableAll": "启用全部",
"error": "错误",
"experimental": "测试版",
"export": "导出",
"extensionName": "扩展名称",
"findIssues": "查找问题",
"firstTimeUIMessage": "这是您第一次使用新界面。选择 \"菜单 > 使用新菜单 > 禁用\" 来恢复旧界面。",
@@ -131,7 +132,8 @@
"systemInfo": "系统信息",
"terminal": "终端",
"upload": "上传",
"videoFailedToLoad": "视频加载失败"
"videoFailedToLoad": "视频加载失败",
"workflow": "工作流"
},
"graphCanvasMenu": {
"fitView": "适应视图",
@@ -204,27 +206,42 @@
"pathValidationFailed": "路径验证失败",
"selectItemsToMigrate": "选择要迁移的项目",
"settings": {
"allowMetrics": "崩溃报告",
"allowMetricsDescription": "发送匿名崩溃报告帮助改ComfyUI。报告不会收集任何个人信息或工作流内容。您可以随时在设置菜单中禁用此功能。",
"allowMetrics": "使用情况指标",
"allowMetricsDescription": "通过发送匿名使用情况指标来帮助改ComfyUI。不会收集任何个人信息或工作流内容。",
"autoUpdate": "自动更新",
"autoUpdateDescription": "更新可用时自动更新。您将在安装更新之前收到通知。",
"dataCollectionDialog": {
"customNodeConfigurations": "自定义节点配置",
"errorReports": "错误信息和堆栈跟踪",
"fileSystemInformation": "文件系统信息",
"personalInformation": "个人信息",
"systemInfo": "硬件、操作系统类型和应用版本",
"collect": {
"errorReports": "错误报告和堆栈跟踪",
"systemInfo": "硬件,操作系统类型和应用版本",
"userJourneyEvents": "用户旅程事件",
"userJourneyTooltip": "用户旅程事件用于跟踪用户通过应用安装过程的旅程。事件收集在第一次成功运行ComfyUI工作流后结束。"
},
"doNotCollect": {
"customNodeConfigurations": "自定义节点配置",
"fileSystemInformation": "文件系统信息",
"personalInformation": "个人信息",
"workflowContents": "工作流内容"
},
"title": "关于数据收集",
"whatWeCollect": "我们收集的内容:",
"whatWeDoNotCollect": "我们不收集的内容:",
"workflowContent": "工作流内容",
"workflowContents": "工作流内容"
"whatWeDoNotCollect": "我们不收集的内容:"
},
"learnMoreAboutData": "了解更多关于数据收集的信息"
},
"systemLocations": "系统位置",
"unhandledError": "未知错误"
},
"issueReport": {
"contactFollowUp": "跟进联系我",
"helpFix": "帮助修复这个",
"notifyResolve": "解决时通知我",
"provideAdditionalDetails": "提供额外的详细信息(可选)",
"provideEmail": "提供您的电子邮件(可选)",
"stackTrace": "堆栈跟踪",
"submitErrorReport": "提交错误报告(可选)",
"systemStats": "系统状态"
},
"menu": {
"autoQueue": "自动执行",
"batchCount": "批次数量",
@@ -257,6 +274,7 @@
"Clear Pending Tasks": "清除待处理任务",
"Clear Workflow": "清除工作流",
"Clipspace": "剪贴空间",
"Close Current Workflow": "关闭当前工作流",
"Collapse/Expand Selected Nodes": "折叠/展开选定节点",
"Comfy-Org Discord": "Comfy-Org Discord",
"ComfyUI Docs": "ComfyUI 文档",
@@ -621,11 +639,13 @@
"workflows": "工作流"
},
"tabMenu": {
"addToBookmarks": "添加到书签",
"closeOtherTabs": "关闭其他标签",
"closeTab": "关闭标签",
"closeTabsToLeft": "关闭左侧标签",
"closeTabsToRight": "关闭右侧标签",
"duplicateTab": "复制标签"
"duplicateTab": "复制标签",
"removeFromBookmarks": "从书签中移除"
},
"templateWorkflows": {
"template": {

View File

@@ -119,6 +119,9 @@
"clip_name": {
"name": "CLIP名称"
},
"device": {
"name": "设备"
},
"type": {
"name": "类型"
}
@@ -1100,6 +1103,15 @@
}
}
},
"DevToolsNodeWithSeedInput": {
"description": "一个带有种子输入的节点",
"display_name": "带种子输入的节点",
"inputs": {
"seed": {
"name": "种子"
}
}
},
"DevToolsNodeWithStringInput": {
"description": "一个具有字符串输入的节点",
"display_name": "具有字符串输入的节点",
@@ -1124,6 +1136,21 @@
}
}
},
"DevToolsObjectPatchNode": {
"description": "一个应用对象补丁的节点",
"display_name": "对象补丁节点",
"inputs": {
"dummy_float": {
"name": "虚拟浮点数"
},
"model": {
"name": "模型"
},
"target_module": {
"name": "目标模块"
}
}
},
"DevToolsSimpleSlider": {
"display_name": "简单滑块",
"inputs": {
@@ -1195,6 +1222,9 @@
"clip_name2": {
"name": "CLIP名称2"
},
"device": {
"name": "设备"
},
"type": {
"name": "类型"
}
@@ -2358,6 +2388,9 @@
"camera_type": {
"name": "相机类型"
},
"fov": {
"name": "视场"
},
"height": {
"name": "高度"
},
@@ -2410,6 +2443,9 @@
"camera_type": {
"name": "相机类型"
},
"fov": {
"name": "视场"
},
"height": {
"name": "高度"
},
@@ -4232,6 +4268,9 @@
"camera_type": {
"name": "相机类型"
},
"fov": {
"name": "视场"
},
"light_intensity": {
"name": "光照强度"
},

View File

@@ -3,7 +3,15 @@
"name": "自动检查更新"
},
"Comfy-Desktop_SendStatistics": {
"name": "发送匿名崩溃报告"
"name": "发送匿名使用情况统计"
},
"Comfy-Desktop_WindowStyle": {
"name": "窗口样式",
"options": {
"custom": "自定义",
"default": "默认"
},
"tooltip": "选择自定义选项以隐藏系统标题栏"
},
"Comfy_ConfirmClear": {
"name": "清除工作流时需要确认"
@@ -60,9 +68,6 @@
"Comfy_Group_DoubleClickTitleToEdit": {
"name": "双击组标题以编辑"
},
"Comfy_InvertMenuScrolling": {
"name": "反转上下文菜单滚动"
},
"Comfy_LinkRelease_Action": {
"name": "释放连线时的操作",
"options": {
@@ -296,7 +301,8 @@
"name": "已打开工作流的位置",
"options": {
"Sidebar": "侧边栏",
"Topbar": "顶部栏"
"Topbar": "顶部栏",
"Topbar (2nd-row)": "顶部栏 (第二行)"
}
},
"LiteGraph_Canvas_MaximumFps": {

View File

@@ -2,6 +2,7 @@
import '@comfyorg/litegraph/style.css'
import { definePreset } from '@primevue/themes'
import Aura from '@primevue/themes/aura'
import * as Sentry from '@sentry/vue'
import { createPinia } from 'pinia'
import 'primeicons/primeicons.css'
import PrimeVue from 'primevue/config'
@@ -24,6 +25,17 @@ const ComfyUIPreset = definePreset(Aura, {
const app = createApp(App)
const pinia = createPinia()
Sentry.init({
app,
dsn: __SENTRY_DSN__,
enabled: __SENTRY_ENABLED__,
release: __COMFYUI_FRONTEND_VERSION__,
integrations: [],
autoSessionTracking: false,
defaultIntegrations: false,
normalizeDepth: 8,
tracesSampleRate: 0
})
app.directive('tooltip', Tooltip)
app
.use(router)

View File

@@ -7,6 +7,7 @@ import type {
ExecutingWsMessage,
ExecutionCachedWsMessage,
ExecutionErrorWsMessage,
ExecutionInterruptedWsMessage,
ExecutionStartWsMessage,
ExecutionSuccessWsMessage,
ExtensionsResponse,
@@ -59,6 +60,7 @@ interface BackendApiCalls {
execution_start: ExecutionStartWsMessage
execution_success: ExecutionSuccessWsMessage
execution_error: ExecutionErrorWsMessage
execution_interrupted: ExecutionInterruptedWsMessage
execution_cached: ExecutionCachedWsMessage
logs: LogsWsMessage
/** Mr Blob Preview, I presume? */
@@ -355,6 +357,7 @@ export class ComfyApi extends EventTarget {
break
case 'execution_start':
case 'execution_error':
case 'execution_interrupted':
case 'execution_cached':
case 'execution_success':
case 'progress':

View File

@@ -49,7 +49,7 @@ import {
} from './pnginfo'
import { $el, ComfyUI } from './ui'
import { ComfyAppMenu } from './ui/menu/index'
import { getStorageValue } from './utils'
import { clone, getStorageValue } from './utils'
import { type ComfyWidgetConstructor, ComfyWidgets } from './widgets'
export const ANIM_PREVIEW_WIDGET = '$$comfy_animation_preview'
@@ -655,7 +655,7 @@ export class ComfyApp {
const keyCombo = KeyComboImpl.fromEvent(e)
const keybindingStore = useKeybindingStore()
const keybinding = keybindingStore.getKeybinding(keyCombo)
if (keybinding && keybinding.targetSelector === '#graph-canvas') {
if (keybinding && keybinding.targetElementId === 'graph-canvas') {
useCommandStore().execute(keybinding.commandId)
block_default = true
}
@@ -1271,11 +1271,7 @@ export class ComfyApp {
reset_invalid_values = true
}
if (typeof structuredClone === 'undefined') {
graphData = JSON.parse(JSON.stringify(graphData))
} else {
graphData = structuredClone(graphData)
}
graphData = clone(graphData)
if (useSettingStore().get('Comfy.Validation.Workflows')) {
// TODO: Show validation error in a dialog.
@@ -1482,7 +1478,7 @@ export class ComfyApp {
* @returns The workflow and node links
*/
async graphToPrompt(graph = this.graph, clean = true) {
for (const outerNode of this.graph.computeExecutionOrder(false)) {
for (const outerNode of graph.computeExecutionOrder(false)) {
if (outerNode.widgets) {
for (const widget of outerNode.widgets) {
// Allow widgets to run callbacks before a prompt has been queued

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