Compare commits
118 Commits
v1.6.13
...
changelog-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11c46ea62d | ||
|
|
86789ceb9d | ||
|
|
1a4e77a3ab | ||
|
|
de570712df | ||
|
|
44612e8f97 | ||
|
|
3df911c1bf | ||
|
|
af26b9ad6d | ||
|
|
d503873980 | ||
|
|
842a9f74fc | ||
|
|
29551a36b3 | ||
|
|
d6e5c8950c | ||
|
|
ad1c1ce9c2 | ||
|
|
cb9d2c6bae | ||
|
|
7fd41eeaba | ||
|
|
79fee6ac72 | ||
|
|
edd58cd153 | ||
|
|
e153508955 | ||
|
|
237fca0bf1 | ||
|
|
65542b885a | ||
|
|
f739f704af | ||
|
|
37abdbe35d | ||
|
|
ff445f5c95 | ||
|
|
84b652a281 | ||
|
|
184291d21b | ||
|
|
d7fb25a36a | ||
|
|
c039a60fcc | ||
|
|
3b6108c26e | ||
|
|
49bb247526 | ||
|
|
dd005f5fa5 | ||
|
|
bf90b458d3 | ||
|
|
7e78c5b1dc | ||
|
|
c13190cd07 | ||
|
|
00f031e382 | ||
|
|
04153caaf5 | ||
|
|
210bfdeb7d | ||
|
|
ce0726d85e | ||
|
|
dd69f9dc30 | ||
|
|
3f261f0e53 | ||
|
|
3b2cc23f65 | ||
|
|
c50a86b258 | ||
|
|
1a8c2bba42 | ||
|
|
fc09951b3e | ||
|
|
76d5f39607 | ||
|
|
9d3bc0f173 | ||
|
|
d9b350e159 | ||
|
|
44610674ee | ||
|
|
9bfce5b8d0 | ||
|
|
8986fa356a | ||
|
|
0c4fd4af1c | ||
|
|
30cd46ce1f | ||
|
|
3122c33310 | ||
|
|
91d8d04dc6 | ||
|
|
8f5aa1ff08 | ||
|
|
e076783b89 | ||
|
|
04c23001fc | ||
|
|
cb265fb0bf | ||
|
|
e9211fe377 | ||
|
|
ffc7febeac | ||
|
|
b15e626607 | ||
|
|
906b5e35a3 | ||
|
|
e8cd9c7642 | ||
|
|
93e184e379 | ||
|
|
1d02cd3c47 | ||
|
|
b86e3f71cb | ||
|
|
1ece2462bd | ||
|
|
73ecacfa2d | ||
|
|
67e6df7c72 | ||
|
|
7e8510028d | ||
|
|
dd4dd8b68a | ||
|
|
7111022617 | ||
|
|
daee073045 | ||
|
|
a1a834a76d | ||
|
|
c437d32691 | ||
|
|
0130d41be5 | ||
|
|
527561d148 | ||
|
|
1c4481c342 | ||
|
|
07000a23d4 | ||
|
|
ea6c9e7ca5 | ||
|
|
90698fced6 | ||
|
|
9716aea10d | ||
|
|
077ded2cce | ||
|
|
ffb20b8789 | ||
|
|
549a2fdc92 | ||
|
|
e123295423 | ||
|
|
613b44610a | ||
|
|
e82d795ff9 | ||
|
|
ba31f8fa68 | ||
|
|
f2eb4e1519 | ||
|
|
975c2248c5 | ||
|
|
477f4b275d | ||
|
|
179b8c22a9 | ||
|
|
6525389273 | ||
|
|
59fc5ac77e | ||
|
|
ed844e04b8 | ||
|
|
972af1977d | ||
|
|
5ac0b7b181 | ||
|
|
31ea39e44c | ||
|
|
e65653c107 | ||
|
|
e46706777c | ||
|
|
1a817a48cb | ||
|
|
871967349f | ||
|
|
11258f4a95 | ||
|
|
c3e05c2a10 | ||
|
|
ea1e776dcc | ||
|
|
f0c273f845 | ||
|
|
ea489851ed | ||
|
|
5717c33a0b | ||
|
|
c31919c418 | ||
|
|
2e7de4701e | ||
|
|
f53723da0f | ||
|
|
5c7cbe968e | ||
|
|
cb607ee070 | ||
|
|
f4b5677901 | ||
|
|
9c1eacf0af | ||
|
|
bb988edf9f | ||
|
|
39e9f421f4 | ||
|
|
3d2b9a8d9d | ||
|
|
c77a5cab5b |
43
.cusorrules
Normal file
@@ -0,0 +1,43 @@
|
||||
// Vue 3 Composition API .cursorrules
|
||||
|
||||
// Vue 3 Composition API best practices
|
||||
const vue3CompositionApiBestPractices = [
|
||||
"Use setup() function for component logic",
|
||||
"Utilize ref and reactive for reactive state",
|
||||
"Implement computed properties with computed()",
|
||||
"Use watch and watchEffect for side effects",
|
||||
"Implement lifecycle hooks with onMounted, onUpdated, etc.",
|
||||
"Utilize provide/inject for dependency injection",
|
||||
]
|
||||
|
||||
// Folder structure
|
||||
const folderStructure = `
|
||||
src/
|
||||
components/
|
||||
constants/
|
||||
hooks/
|
||||
views/
|
||||
stores/
|
||||
services/
|
||||
App.vue
|
||||
main.ts
|
||||
`;
|
||||
|
||||
// Tailwind CSS best practices
|
||||
const tailwindCssBestPractices = [
|
||||
"Use Tailwind CSS for styling",
|
||||
"Implement responsive design with Tailwind CSS",
|
||||
]
|
||||
|
||||
// Additional instructions
|
||||
const additionalInstructions = `
|
||||
1. Leverage VueUse functions for performance-enhancing styles
|
||||
2. Use lodash for utility functions
|
||||
3. Use TypeScript for type safety
|
||||
4. Implement proper props and emits definitions
|
||||
5. Utilize Vue 3's Teleport component when needed
|
||||
6. Use Suspense for async components
|
||||
7. Implement proper error handling
|
||||
8. Follow Vue 3 style guide and naming conventions
|
||||
9. Use Vite for fast development and building
|
||||
`;
|
||||
48
.github/workflows/i18n-node-defs.yaml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Update Node Definitions Locales
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
trigger_type:
|
||||
description: 'Type of trigger (manual or automatic)'
|
||||
required: false
|
||||
type: string
|
||||
default: 'manual'
|
||||
|
||||
jobs:
|
||||
update-locales:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Start dev server
|
||||
# Run electron dev server as it is a superset of the web dev server
|
||||
# We do want electron specific UIs to be translated.
|
||||
run: npm run dev:electron &
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Update en.json
|
||||
run: npm run collect-i18n -- scripts/collect-i18n-node-defs.ts
|
||||
env:
|
||||
PLAYWRIGHT_TEST_URL: http://localhost:5173
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Update translations
|
||||
run: npm run locale
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
commit-message: "Update locales for node definitions"
|
||||
title: "Update locales for node definitions"
|
||||
body: |
|
||||
Automated PR to update locales for node definitions
|
||||
|
||||
This PR was created automatically by the frontend update workflow.
|
||||
branch: update-locales-node-defs-${{ github.event.inputs.trigger_type }}-${{ github.run_id }}
|
||||
base: main
|
||||
labels: dependencies
|
||||
path: ComfyUI_frontend
|
||||
7
.github/workflows/i18n.yaml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/release.yaml
vendored
@@ -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
@@ -41,4 +41,7 @@ browser_tests/*/*-win32.png
|
||||
|
||||
dist.zip
|
||||
|
||||
/temp/
|
||||
/temp/
|
||||
|
||||
# Generated JSON Schemas
|
||||
/schemas/
|
||||
|
||||
12
.prettierrc
@@ -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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
59
README.md
@@ -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.
|
||||

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

|
||||
|
||||
</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!')
|
||||

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

|
||||
</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>`
|
||||
|
||||
37
browser_tests/assets/collapsed_multiline.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"last_node_id": 1,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [20, 50],
|
||||
"size": [400, 200],
|
||||
"flags": { "collapsed": true },
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": null,
|
||||
"localized_name": "clip"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": null,
|
||||
"localized_name": "CONDITIONING"
|
||||
}
|
||||
],
|
||||
"properties": {},
|
||||
"widgets_values": ["Should not be displayed."]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"version": 0.4
|
||||
}
|
||||
BIN
browser_tests/assets/image32x32.webp
Normal file
|
After Width: | Height: | Size: 488 B |
BIN
browser_tests/assets/image64x64.webp
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
@@ -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,17 +129,39 @@ 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('Color Palette', () => {
|
||||
test('Can show custom color palette', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.CustomColorPalettes', customColorPalettes)
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'custom_obsidian_dark')
|
||||
await comfyPage.nextFrame()
|
||||
// Reload to apply the new setting. Setting Comfy.CustomColorPalettes directly
|
||||
// doesn't update the store immediately.
|
||||
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')
|
||||
@@ -150,6 +173,12 @@ test.describe('Color Palette', () => {
|
||||
}, customColorPalettes.obsidian_dark)
|
||||
expect(await comfyPage.getToastErrorCount()).toBe(0)
|
||||
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'custom-color-palette-obsidian-dark.png'
|
||||
)
|
||||
// Legacy `custom_` prefix is still supported
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'custom_obsidian_dark')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
|
||||
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 98 KiB |
@@ -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()
|
||||
|
||||
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 114 KiB |
@@ -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))
|
||||
})
|
||||
})
|
||||
|
||||
27
browser_tests/domWidget.spec.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
test.describe('DOM Widget', () => {
|
||||
test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('collapsed_multiline')
|
||||
|
||||
expect(comfyPage.page.locator('.comfy-multiline-input')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Multiline textarea correctly collapses', async ({ comfyPage }) => {
|
||||
const multilineTextAreas = comfyPage.page.locator('.comfy-multiline-input')
|
||||
const firstMultiline = multilineTextAreas.first()
|
||||
const lastMultiline = multilineTextAreas.last()
|
||||
|
||||
await expect(firstMultiline).toBeVisible()
|
||||
await expect(lastMultiline).toBeVisible()
|
||||
|
||||
const nodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
|
||||
for (const node of nodes) {
|
||||
await node.click('collapse')
|
||||
}
|
||||
await expect(firstMultiline).not.toBeVisible()
|
||||
await expect(lastMultiline).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -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() {
|
||||
@@ -108,6 +117,8 @@ class ConfirmDialog {
|
||||
}
|
||||
|
||||
export class ComfyPage {
|
||||
private _history: TaskHistory | null = null
|
||||
|
||||
public readonly url: string
|
||||
// All canvas position operations are based on default view of canvas.
|
||||
public readonly canvas: Locator
|
||||
@@ -242,7 +253,8 @@ export class ComfyPage {
|
||||
}
|
||||
|
||||
setupHistory(): TaskHistory {
|
||||
return new TaskHistory(this)
|
||||
this._history ??= new TaskHistory(this)
|
||||
return this._history
|
||||
}
|
||||
|
||||
async setup({ clearStorage = true }: { clearStorage?: boolean } = {}) {
|
||||
@@ -357,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)
|
||||
}
|
||||
@@ -818,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(
|
||||
(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
@@ -187,6 +194,10 @@ export class QueueSidebarTab extends SidebarTab {
|
||||
return this.root.locator('.no-results-placeholder')
|
||||
}
|
||||
|
||||
get galleryImage() {
|
||||
return this.page.locator('.galleria-image')
|
||||
}
|
||||
|
||||
private getToggleExpandButton(isExpanded: boolean) {
|
||||
const iconSelector = isExpanded ? '.pi-image' : '.pi-images'
|
||||
return this.root.locator(`.toggle-expanded-button ${iconSelector}`)
|
||||
@@ -242,4 +253,31 @@ export class QueueSidebarTab extends SidebarTab {
|
||||
localStorage.setItem('queue', JSON.stringify([width, 100 - width]))
|
||||
}, width)
|
||||
}
|
||||
|
||||
getTaskPreviewButton(taskIndex: number) {
|
||||
return this.tasks.nth(taskIndex).getByRole('button')
|
||||
}
|
||||
|
||||
async openTaskPreview(taskIndex: number) {
|
||||
const previewButton = this.getTaskPreviewButton(taskIndex)
|
||||
await previewButton.click()
|
||||
return this.galleryImage.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
getGalleryImage(imageFilename: string) {
|
||||
return this.galleryImage.and(this.page.getByAltText(imageFilename))
|
||||
}
|
||||
|
||||
getTaskImage(imageFilename: string) {
|
||||
return this.tasks.getByAltText(imageFilename)
|
||||
}
|
||||
|
||||
/** Trigger the queue store and tasks to update */
|
||||
async triggerTasksUpdate() {
|
||||
await this.page.evaluate(() => {
|
||||
window['app']['api'].dispatchCustomEvent('status', {
|
||||
exec_info: { queue_remaining: 0 }
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -132,11 +132,12 @@ export default class TaskHistory {
|
||||
private addTask(task: HistoryTaskItem) {
|
||||
setPromptId(task)
|
||||
setQueueIndex(task)
|
||||
this.tasks.push(task)
|
||||
this.tasks.unshift(task) // Tasks are added to the front of the queue
|
||||
}
|
||||
|
||||
clearTasks() {
|
||||
clearTasks(): this {
|
||||
this.tasks = []
|
||||
return this
|
||||
}
|
||||
|
||||
withTask(
|
||||
@@ -155,7 +156,7 @@ export default class TaskHistory {
|
||||
/** Repeats the last task in the task history a specified number of times. */
|
||||
repeat(n: number): this {
|
||||
for (let i = 0; i < n; i++)
|
||||
this.addTask(structuredClone(this.tasks.at(-1)) as HistoryTaskItem)
|
||||
this.addTask(structuredClone(this.tasks.at(0)) as HistoryTaskItem)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -18,7 +18,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()
|
||||
@@ -289,6 +289,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', {
|
||||
@@ -396,6 +437,56 @@ test.describe('Menu', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('Can duplicate workflow', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
await comfyPage.menu.topbar.saveWorkflow('workflow1.json')
|
||||
|
||||
expect(await tab.getTopLevelSavedWorkflowNames()).toEqual(
|
||||
expect.arrayContaining(['workflow1.json'])
|
||||
)
|
||||
|
||||
await comfyPage.executeCommand('Comfy.DuplicateWorkflow')
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'workflow1.json',
|
||||
'*workflow1 (Copy).json'
|
||||
])
|
||||
|
||||
await comfyPage.executeCommand('Comfy.DuplicateWorkflow')
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'workflow1.json',
|
||||
'*workflow1 (Copy).json',
|
||||
'*workflow1 (Copy) (2).json'
|
||||
])
|
||||
|
||||
await comfyPage.executeCommand('Comfy.DuplicateWorkflow')
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'workflow1.json',
|
||||
'*workflow1 (Copy).json',
|
||||
'*workflow1 (Copy) (2).json',
|
||||
'*workflow1 (Copy) (3).json'
|
||||
])
|
||||
})
|
||||
|
||||
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
|
||||
}) => {
|
||||
@@ -475,7 +566,7 @@ test.describe('Menu', () => {
|
||||
})
|
||||
|
||||
await comfyPage.setSetting('Comfy.Locale', 'zh')
|
||||
await comfyPage.reload()
|
||||
await comfyPage.setup()
|
||||
|
||||
const downloadedContentZh = await comfyPage.getExportedWorkflow({
|
||||
api: false
|
||||
@@ -571,6 +662,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)
|
||||
|
||||
@@ -705,7 +805,7 @@ test.describe('Menu', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Queue sidebar', () => {
|
||||
test.describe.skip('Queue sidebar', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
@@ -843,4 +943,65 @@ test.describe('Queue sidebar', () => {
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Gallery', () => {
|
||||
const firstImage = 'example.webp'
|
||||
const secondImage = 'image32x32.webp'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage
|
||||
.setupHistory()
|
||||
.withTask([secondImage])
|
||||
.withTask([firstImage])
|
||||
.setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
await comfyPage.menu.queueTab.openTaskPreview(0)
|
||||
})
|
||||
|
||||
test('displays gallery image after opening task preview', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.nextFrame()
|
||||
expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible()
|
||||
})
|
||||
|
||||
test('maintains active gallery item when new tasks are added', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Add a new task while the gallery is still open
|
||||
const newImage = 'image64x64.webp'
|
||||
comfyPage.setupHistory().withTask([newImage])
|
||||
await comfyPage.menu.queueTab.triggerTasksUpdate()
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
const newTask = comfyPage.menu.queueTab.tasks.getByAltText(newImage)
|
||||
await newTask.waitFor({ state: 'visible' })
|
||||
// The active gallery item should still be the initial image
|
||||
expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible()
|
||||
})
|
||||
|
||||
test.describe('Gallery navigation', () => {
|
||||
const paths: {
|
||||
description: string
|
||||
path: ('Right' | 'Left')[]
|
||||
end: string
|
||||
}[] = [
|
||||
{ description: 'Right', path: ['Right'], end: secondImage },
|
||||
{ description: 'Left', path: ['Right', 'Left'], end: firstImage },
|
||||
{ description: 'Left wrap', path: ['Left'], end: secondImage },
|
||||
{ description: 'Right wrap', path: ['Right', 'Right'], end: firstImage }
|
||||
]
|
||||
|
||||
paths.forEach(({ description, path, end }) => {
|
||||
test(`can navigate gallery ${description}`, async ({ comfyPage }) => {
|
||||
for (const direction of path)
|
||||
await comfyPage.page.keyboard.press(`Arrow${direction}`, {
|
||||
delay: 256
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
expect(comfyPage.menu.queueTab.getGalleryImage(end)).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 105 KiB |
@@ -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
@@ -1 +1,3 @@
|
||||
declare const __COMFYUI_FRONTEND_VERSION__: string
|
||||
declare const __SENTRY_ENABLED__: boolean
|
||||
declare const __SENTRY_DSN__: string
|
||||
|
||||
130
package-lock.json
generated
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.6.13",
|
||||
"version": "1.7.14",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.6.13",
|
||||
"version": "1.7.14",
|
||||
"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.53",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.10",
|
||||
"@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,14 @@
|
||||
"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==",
|
||||
"license": "GPL-3.0-only"
|
||||
"version": "0.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.10.tgz",
|
||||
"integrity": "sha512-UWBgyuWeV7vussYZVUYhCe0jj+XbIq2nglrCUy6IgFgXp9pbE8Ktg5D36WxE0RWj6SvVXErlCL9wWnMktaRbCA=="
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.8.53",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.53.tgz",
|
||||
"integrity": "sha512-ihgHGAFVWzeWobhYA4pLRIlqykGwznZQM9gq2KRMs6FOYW1TrbFjbI2GvXZs93wkzXkoY8swwsQitBD7MklT3w==",
|
||||
"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 +4462,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 +19531,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": {
|
||||
|
||||
17
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.6.13",
|
||||
"version": "1.7.14",
|
||||
"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.53",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.10",
|
||||
"@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",
|
||||
|
||||
@@ -8,7 +8,7 @@ const config: PlaywrightTestConfig = {
|
||||
},
|
||||
reporter: 'list',
|
||||
timeout: 10000,
|
||||
testMatch: /collect-i18n\.ts/
|
||||
testMatch: /collect-i18n-.*\.ts/
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
125
scripts/collect-i18n-node-defs.ts
Normal 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))
|
||||
})
|
||||
35
scripts/generate-json-schema.ts
Normal 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!')
|
||||
@@ -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)
|
||||
1
src/assets/changelog.json
Normal file
@@ -0,0 +1 @@
|
||||
{"last_node_id":16,"last_link_id":10,"nodes":[{"id":16,"type":"MarkdownNote","pos":[630,60],"size":[930,945],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[],"title":"Changelog","properties":{},"widgets_values":["# v0.3.11 (Pre-release)\n\n## What's Changed\n\n* Nvidia Cosmos 7B and 14B: text to video and image to video diffusion model support.\n <iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/watch?v=-OCwHBur0FM\" frameborder=\"0\" allowfullscreen></iframe>\n\n\n* New sampler: res_multistep\n* ckpt/pt/etc.. files are now always loaded safely on pytorch 2.4 and above.\n* Fix some cases of ancestral samplers not being deterministic.\n* Support ascend npu by @ji-huazhong [#5436](https://github.com/comfyanonymous/ComfyUI/pull/5436)\n* Add option to log non-error output to stdout by @webfiltered [#6243](https://github.com/comfyanonymous/ComfyUI/pull/6243)\n* serve workflow templates from custom_nodes by @bezo97 [#6193](https://github.com/comfyanonymous/ComfyUI/pull/6193)\n* Remove duplicate calls to INPUT_TYPES by @catboxanon [#6249](https://github.com/comfyanonymous/ComfyUI/pull/6249)\n* Fix Hook Keyframe 'guarantee_steps' behavior and add 'sigmas' by @Kosinkadink [#6273](https://github.com/comfyanonymous/ComfyUI/pull/6273)\n* Add kl_optimal scheduler by @blepping [#6206](https://github.com/comfyanonymous/ComfyUI/pull/6206)\n* (fix): \"verbose\" argument by @bigcat88 [#6289](https://github.com/comfyanonymous/ComfyUI/pull/6289)\n* Fix custom node type-hinting examples by @webfiltered [#6281](https://github.com/comfyanonymous/ComfyUI/pull/6281)\n* Add missing model_options param to finalize_default_conds call by @Kosinkadink [#6296](https://github.com/comfyanonymous/ComfyUI/pull/6296)\n* Fix unknown scheduler error handling in calculate_sigmas function by @blepping [#6280](https://github.com/comfyanonymous/ComfyUI/pull/6280)\n* Fix temporal tiling for Tiled VAE decoder, remove redundant tiles. by @kvochko [#6306](https://github.com/comfyanonymous/ComfyUI/pull/6306)\n* Update web content to release v1.6.14 by @huchenlei [#6312](https://github.com/comfyanonymous/ComfyUI/pull/6312)\n* add fov and mask for load 3d node by @jtydhr88 [#6308](https://github.com/comfyanonymous/ComfyUI/pull/6308)\n* Update web content to release v1.6.15 by @huchenlei [#6324](https://github.com/comfyanonymous/ComfyUI/pull/6324)\n* Update web content to release v1.6.16 by @huchenlei [#6335](https://github.com/comfyanonymous/ComfyUI/pull/6335)\n* Update web content to release v1.6.17 by @huchenlei [#6337](https://github.com/comfyanonymous/ComfyUI/pull/6337)\n* Add update-frontend github action by @huchenlei [#6336](https://github.com/comfyanonymous/ComfyUI/pull/6336)\n* Update CODEOWNERS by @yoland68 [#6338](https://github.com/comfyanonymous/ComfyUI/pull/6338)\n* In inner_sample, change \"sigmas\" to \"sample_sigmas\" by @Kosinkadink [#6360](https://github.com/comfyanonymous/ComfyUI/pull/6360)\n* Frontend Update: v1.6.18 by @huchenlei [#6368](https://github.com/comfyanonymous/ComfyUI/pull/6368)\n* Document get_attr and get_model_object by @huchenlei [#6357](https://github.com/comfyanonymous/ComfyUI/pull/6357)\n* fixed: robust loading \\`comfy.settings.json\\` by @ltdrdata [#6383](https://github.com/comfyanonymous/ComfyUI/pull/6383)\n* Add pyproject.toml by @huchenlei [#6386](https://github.com/comfyanonymous/ComfyUI/pull/6386)\n* Hooks Part 2 - TransformerOptionsHook and AdditionalModelsHook by @Kosinkadink [#6377](https://github.com/comfyanonymous/ComfyUI/pull/6377)\n* Merge ruff.toml into pyproject.toml by @huchenlei [#6431](https://github.com/comfyanonymous/ComfyUI/pull/6431)\n* (fix): load_extra_path_config: relative path not converted to a full path by @bigcat88 [#6395](https://github.com/comfyanonymous/ComfyUI/pull/6395)\n* Rewrite res_multistep sampler and implement res_multistep_cfg_pp sampler by @pamparamm [#6462](https://github.com/comfyanonymous/ComfyUI/pull/6462)\n* Add SetFirstSigma node by @catboxanon [#6459](https://github.com/comfyanonymous/ComfyUI/pull/6459)\n\n**Full Changelog**: [\\`v0.3.10...v0.3.11\\`](https://github.com/comfyanonymous/ComfyUI/compare/v0.3.10...v0.3.11)\n\n----\n\n> To disable showing changelog on new releases, go the the Changelog section in settings\n"],"color":"#222","bgcolor":"#000"}],"links":[],"groups":[],"config":{},"extra":{"ds":{"scale":1.503752370924104,"offset":[-489.9627968865069,158.59081129748483]}},"version":0.4}
|
||||
@@ -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 */
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -20,11 +20,16 @@ const terminalCreated = (
|
||||
let offData: IDisposable
|
||||
let offOutput: () => void
|
||||
|
||||
useAutoSize(root, true, true, () => {
|
||||
// If we aren't visible, don't resize
|
||||
if (!terminal.element?.offsetParent) return
|
||||
useAutoSize({
|
||||
root,
|
||||
autoRows: true,
|
||||
autoCols: true,
|
||||
onResize: () => {
|
||||
// If we aren't visible, don't resize
|
||||
if (!terminal.element?.offsetParent) return
|
||||
|
||||
terminalApi.resize(terminal.cols, terminal.rows)
|
||||
terminalApi.resize(terminal.cols, terminal.rows)
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -29,7 +29,12 @@ const terminalCreated = (
|
||||
{ terminal, useAutoSize }: ReturnType<typeof useTerminal>,
|
||||
root: Ref<HTMLElement>
|
||||
) => {
|
||||
useAutoSize(root, true, false)
|
||||
// `autoCols` is false because we don't want the progress bar in the terminal
|
||||
// to render incorrectly as the progress bar is rendered based on the
|
||||
// server's terminal size.
|
||||
// Apply a min cols of 80 for colab environments
|
||||
// See https://github.com/comfyanonymous/ComfyUI/issues/6396
|
||||
useAutoSize({ root, autoRows: true, autoCols: false, minCols: 80 })
|
||||
|
||||
const update = (entries: Array<LogEntry>, size?: TerminalSize) => {
|
||||
if (size) {
|
||||
|
||||
40
src/components/common/CheckboxGroup.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div :class="['flex flex-wrap', $attrs.class]">
|
||||
<div
|
||||
v-for="checkbox in checkboxes"
|
||||
:key="checkbox.value"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<Checkbox
|
||||
v-model="internalSelection"
|
||||
:inputId="checkbox.value"
|
||||
:value="checkbox.value"
|
||||
/>
|
||||
<label :for="checkbox.value" class="ml-2">{{ checkbox.label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface CheckboxItem {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
checkboxes: CheckboxItem[]
|
||||
modelValue: string[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string[]): void
|
||||
}>()
|
||||
|
||||
const internalSelection = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: string[]) => emit('update:modelValue', value)
|
||||
})
|
||||
</script>
|
||||
@@ -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'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
:options="colorOptions"
|
||||
optionLabel="name"
|
||||
dataKey="value"
|
||||
:allow-empty="false"
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<div
|
||||
|
||||
@@ -13,11 +13,16 @@
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
||||
if (settingStore.get('Comfy.Window.UnloadConfirmation')) {
|
||||
if (
|
||||
settingStore.get('Comfy.Window.UnloadConfirmation') &&
|
||||
workflowStore.modifiedWorkflows.length > 0
|
||||
) {
|
||||
event.preventDefault()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -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,13 @@
|
||||
</ScrollPanel>
|
||||
<Divider />
|
||||
</template>
|
||||
|
||||
<ReportIssuePanel
|
||||
v-if="sendReportOpen"
|
||||
error-type="graphExecutionError"
|
||||
:extra-fields="[stackTraceField]"
|
||||
:tags="{ exceptionMessage: props.error.exception_message }"
|
||||
/>
|
||||
<div class="action-container">
|
||||
<ReportIssueButton v-if="showSendError" :error="props.error" />
|
||||
<FindIssueButton
|
||||
:errorMessage="props.error.exception_message"
|
||||
:repoOwner="repoOwner"
|
||||
@@ -41,16 +53,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 +77,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 {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
201
src/components/dialog/content/error/ReportIssuePanel.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<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[]
|
||||
tags?: Record<string, string>
|
||||
}>()
|
||||
const {
|
||||
defaultFields = ['Workflow', 'Logs', 'SystemStats', 'Settings'],
|
||||
tags = {}
|
||||
} = 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,
|
||||
...tags
|
||||
},
|
||||
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>
|
||||
@@ -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 })
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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"
|
||||
@@ -42,11 +42,12 @@ import { storeToRefs } from 'pinia'
|
||||
import Button from 'primevue/button'
|
||||
import Message from 'primevue/message'
|
||||
import Select from 'primevue/select'
|
||||
import { watch } from 'vue'
|
||||
|
||||
import { useColorPaletteService } from '@/services/colorPaletteService'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const colorPaletteService = useColorPaletteService()
|
||||
const { palettes, activePaletteId } = storeToRefs(colorPaletteStore)
|
||||
@@ -54,11 +55,7 @@ const { palettes, activePaletteId } = storeToRefs(colorPaletteStore)
|
||||
const importCustomPalette = async () => {
|
||||
const palette = await colorPaletteService.importColorPalette()
|
||||
if (palette) {
|
||||
colorPaletteService.loadColorPalette(palette.id)
|
||||
settingStore.set('Comfy.ColorPalette', palette.id)
|
||||
}
|
||||
}
|
||||
|
||||
watch(activePaletteId, () => {
|
||||
colorPaletteService.loadColorPalette(activePaletteId.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
<BottomPanel />
|
||||
</template>
|
||||
<template #graph-canvas-panel>
|
||||
<SecondRowWorkflowTabs
|
||||
v-if="workflowTabsPosition === 'Topbar (2nd-row)'"
|
||||
/>
|
||||
<GraphCanvasMenu v-if="canvasMenuEnabled" />
|
||||
</template>
|
||||
</LiteGraphCanvasSplitterOverlay>
|
||||
@@ -40,6 +43,7 @@ import {
|
||||
} from '@comfyorg/litegraph'
|
||||
import { computed, onMounted, ref, watch, watchEffect } from 'vue'
|
||||
|
||||
import changelog from '@/assets/changelog.json'
|
||||
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
|
||||
import BottomPanel from '@/components/bottomPanel/BottomPanel.vue'
|
||||
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
|
||||
@@ -48,12 +52,14 @@ 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 { app } 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'
|
||||
@@ -71,6 +77,7 @@ import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import { getComfyVersion, isVersionLessThan } from '@/utils/envUtil'
|
||||
|
||||
const emit = defineEmits(['ready'])
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
@@ -83,11 +90,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) {
|
||||
@@ -223,11 +256,21 @@ watch(
|
||||
)
|
||||
|
||||
const colorPaletteService = useColorPaletteService()
|
||||
watchEffect(() => {
|
||||
if (!canvasStore.canvas) return
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
watch(
|
||||
[() => canvasStore.canvas, () => settingStore.get('Comfy.ColorPalette')],
|
||||
([canvas, currentPaletteId]) => {
|
||||
if (!canvas) return
|
||||
|
||||
colorPaletteService.loadColorPalette(settingStore.get('Comfy.ColorPalette'))
|
||||
})
|
||||
colorPaletteService.loadColorPalette(currentPaletteId)
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => colorPaletteStore.activePaletteId,
|
||||
(newValue) => {
|
||||
settingStore.set('Comfy.ColorPalette', newValue)
|
||||
}
|
||||
)
|
||||
|
||||
const workflowStore = useWorkflowStore()
|
||||
const persistCurrentWorkflow = () => {
|
||||
@@ -238,6 +281,35 @@ const persistCurrentWorkflow = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const stopWatchChangeLog = watch(
|
||||
() => workflowStore.activeWorkflow,
|
||||
async () => {
|
||||
if (!comfyAppReady.value) return
|
||||
if (!changelog || settingStore.get('Comfy.ShowChangeLog') === false) {
|
||||
stopWatchChangeLog()
|
||||
return
|
||||
}
|
||||
|
||||
const workflow = workflowStore.activeWorkflow
|
||||
const activeState = workflow?.activeState
|
||||
const comfyVersion = getComfyVersion() // TODO: initialize/fetch somewhere else
|
||||
if (!workflow || !activeState || !comfyVersion) return
|
||||
|
||||
// Just checking if workflow temporary is not enough bc of Duplicate feature
|
||||
const isBlank = !workflow.isPersisted && activeState.nodes?.length === 0
|
||||
if (!isBlank) return
|
||||
|
||||
const lastShown = settingStore.get('Comfy.LastChangelogVersion')
|
||||
const isSeen = lastShown && !isVersionLessThan(lastShown, comfyVersion)
|
||||
if (!isSeen) {
|
||||
app.loadGraphData(changelog)
|
||||
settingStore.set('Comfy.LastChangelogVersion', comfyVersion)
|
||||
}
|
||||
|
||||
stopWatchChangeLog()
|
||||
}
|
||||
)
|
||||
|
||||
watchEffect(() => {
|
||||
if (workflowStore.activeWorkflow) {
|
||||
const workflow = workflowStore.activeWorkflow
|
||||
@@ -343,11 +415,22 @@ onMounted(async () => {
|
||||
comfyAppReady.value = true
|
||||
|
||||
// Load color palette
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
colorPaletteStore.customPalettes = settingStore.get(
|
||||
'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'),
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -59,10 +59,19 @@
|
||||
</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'
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -72,25 +81,43 @@
|
||||
<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>
|
||||
</ul>
|
||||
|
||||
<div class="mt-4">
|
||||
<a
|
||||
href="https://comfy.org/privacy"
|
||||
target="_blank"
|
||||
class="text-blue-400 hover:text-blue-300 underline"
|
||||
>
|
||||
{{ $t('install.settings.dataCollectionDialog.viewFullPolicy') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
@@ -130,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()
|
||||
|
||||
@@ -158,8 +159,8 @@ const pickGpu = (value: typeof selected.value) => {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="postcss">
|
||||
:root {
|
||||
<style scoped>
|
||||
.p-tag {
|
||||
--p-tag-gap: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<teleport :to="teleportTarget">
|
||||
<nav :class="'side-tool-bar-container' + (isSmall ? ' small-sidebar' : '')">
|
||||
<nav class="side-tool-bar-container" :class="{ 'small-sidebar': isSmall }">
|
||||
<SidebarIcon
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
@@ -69,17 +69,6 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--sidebar-width: 64px;
|
||||
--sidebar-icon-size: 1.5rem;
|
||||
}
|
||||
:root .small-sidebar {
|
||||
--sidebar-width: 40px;
|
||||
--sidebar-icon-size: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.side-tool-bar-container {
|
||||
display: flex;
|
||||
@@ -94,6 +83,14 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
|
||||
background-color: var(--comfy-menu-secondary-bg);
|
||||
color: var(--fg-color);
|
||||
box-shadow: var(--bar-shadow);
|
||||
|
||||
--sidebar-width: 4rem;
|
||||
--sidebar-icon-size: 1.5rem;
|
||||
}
|
||||
|
||||
.side-tool-bar-container.small-sidebar {
|
||||
--sidebar-width: 2.5rem;
|
||||
--sidebar-icon-size: 1rem;
|
||||
}
|
||||
|
||||
.side-tool-bar-end {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -99,7 +99,7 @@ import type { MenuItem } from 'primevue/menuitem'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { useConfirm } from 'primevue/useconfirm'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { computed, ref, shallowRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
@@ -108,7 +108,11 @@ import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { TaskItemImpl, useQueueStore } from '@/stores/queueStore'
|
||||
import {
|
||||
ResultItemImpl,
|
||||
TaskItemImpl,
|
||||
useQueueStore
|
||||
} from '@/stores/queueStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { ComfyNode } from '@/types/comfyWorkflow'
|
||||
|
||||
@@ -127,6 +131,7 @@ const { t } = useI18n()
|
||||
// Expanded view: show all outputs in a flat list.
|
||||
const isExpanded = ref(false)
|
||||
const galleryActiveIndex = ref(-1)
|
||||
const allGalleryItems = shallowRef<ResultItemImpl[]>([])
|
||||
// Folder view: only show outputs from a single selected task.
|
||||
const folderTask = ref<TaskItemImpl | null>(null)
|
||||
const isInFolderView = computed(() => folderTask.value !== null)
|
||||
@@ -141,12 +146,12 @@ const allTasks = computed(() =>
|
||||
? queueStore.flatTasks
|
||||
: queueStore.tasks
|
||||
)
|
||||
const allGalleryItems = computed(() =>
|
||||
allTasks.value.flatMap((task: TaskItemImpl) => {
|
||||
const updateGalleryItems = () => {
|
||||
allGalleryItems.value = allTasks.value.flatMap((task: TaskItemImpl) => {
|
||||
const previewOutput = task.previewOutput
|
||||
return previewOutput ? [previewOutput] : []
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const toggleExpanded = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
@@ -189,10 +194,6 @@ const confirmRemoveAll = (event: Event) => {
|
||||
})
|
||||
}
|
||||
|
||||
const onStatus = async () => {
|
||||
await queueStore.update()
|
||||
}
|
||||
|
||||
const menu = ref(null)
|
||||
const menuTargetTask = ref<TaskItemImpl | null>(null)
|
||||
const menuTargetNode = ref<ComfyNode | null>(null)
|
||||
@@ -232,6 +233,7 @@ const handleContextMenu = ({
|
||||
}
|
||||
|
||||
const handlePreview = (task: TaskItemImpl) => {
|
||||
updateGalleryItems()
|
||||
galleryActiveIndex.value = allGalleryItems.value.findIndex(
|
||||
(item) => item.url === task.previewOutput?.url
|
||||
)
|
||||
@@ -249,12 +251,16 @@ const toggleImageFit = () => {
|
||||
settingStore.set(IMAGE_FIT, imageFit.value === 'cover' ? 'contain' : 'cover')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
api.addEventListener('status', onStatus)
|
||||
queueStore.update()
|
||||
})
|
||||
watch(allTasks, () => {
|
||||
const isGalleryOpen = galleryActiveIndex.value !== -1
|
||||
if (!isGalleryOpen) return
|
||||
|
||||
onUnmounted(() => {
|
||||
api.removeEventListener('status', onStatus)
|
||||
const prevLength = allGalleryItems.value.length
|
||||
updateGalleryItems()
|
||||
const lengthChange = allGalleryItems.value.length - prevLength
|
||||
if (!lengthChange) return
|
||||
|
||||
const newIndex = galleryActiveIndex.value + lengthChange
|
||||
galleryActiveIndex.value = Math.max(0, newIndex)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
:key="item.url"
|
||||
:src="item.url"
|
||||
:contain="false"
|
||||
:alt="item.filename"
|
||||
class="galleria-image"
|
||||
v-if="item.isImage"
|
||||
/>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
15
src/components/topbar/SecondRowWorkflowTabs.vue
Normal 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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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',
|
||||
@@ -229,7 +230,8 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
id: 'Comfy.Window.UnloadConfirmation',
|
||||
name: 'Show confirmation when closing window',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
defaultValue: true,
|
||||
versionModified: '1.7.12'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.TreeExplorer.ItemPadding',
|
||||
@@ -399,8 +401,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 +427,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',
|
||||
@@ -672,7 +685,11 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
name: 'The active color palette id',
|
||||
type: 'hidden',
|
||||
defaultValue: 'dark',
|
||||
versionModified: '1.6.7'
|
||||
versionModified: '1.6.7',
|
||||
migrateDeprecatedValue(value: string) {
|
||||
// Legacy custom palettes were prefixed with 'custom_'
|
||||
return value.startsWith('custom_') ? value.replace('custom_', '') : value
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.CustomColorPalettes',
|
||||
@@ -691,5 +708,20 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
defaultValue: 'after',
|
||||
options: ['before', 'after'],
|
||||
versionModified: '1.6.10'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.ShowChangeLog',
|
||||
category: ['Comfy', 'Changelog'],
|
||||
name: 'Show changelog on new release',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.7.15'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.LastChangelogVersion',
|
||||
name: 'Last shown changelog version',
|
||||
type: 'hidden',
|
||||
defaultValue: '0.0.0',
|
||||
versionAdded: '1.7.15'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '../../scripts/app'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { processDynamicPrompt } from '@/utils/formatUtil'
|
||||
|
||||
// Allows for simple dynamic prompt replacement
|
||||
// Inputs in the format {a|b} will have a random value of a or b chosen when the prompt is queued.
|
||||
|
||||
/*
|
||||
* Strips C-style line and block comments from a string
|
||||
*/
|
||||
function stripComments(str) {
|
||||
return str.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '')
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
useExtensionService().registerExtension({
|
||||
name: 'Comfy.DynamicPrompts',
|
||||
nodeCreated(node) {
|
||||
if (node.widgets) {
|
||||
@@ -23,25 +16,9 @@ app.registerExtension({
|
||||
// Override the serialization of the value to resolve dynamic prompts for all widgets supporting it in this node
|
||||
// @ts-expect-error hacky override
|
||||
widget.serializeValue = (workflowNode, widgetIndex) => {
|
||||
let prompt = stripComments(widget.value)
|
||||
while (
|
||||
prompt.replace('\\{', '').includes('{') &&
|
||||
prompt.replace('\\}', '').includes('}')
|
||||
) {
|
||||
const startIndex = prompt.replace('\\{', '00').indexOf('{')
|
||||
const endIndex = prompt.replace('\\}', '00').indexOf('}')
|
||||
if (typeof widget.value !== 'string') return widget.value
|
||||
|
||||
const optionsString = prompt.substring(startIndex + 1, endIndex)
|
||||
const options = optionsString.split('|')
|
||||
|
||||
const randomIndex = Math.floor(Math.random() * options.length)
|
||||
const randomOption = options[randomIndex]
|
||||
|
||||
prompt =
|
||||
prompt.substring(0, startIndex) +
|
||||
randomOption +
|
||||
prompt.substring(endIndex + 1)
|
||||
}
|
||||
const prompt = processDynamicPrompt(widget.value)
|
||||
|
||||
// Overwrite the value in the serialized workflow pnginfo
|
||||
if (workflowNode?.widgets_values)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -163,6 +181,16 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
|
||||
}
|
||||
],
|
||||
|
||||
keybindings: [
|
||||
{
|
||||
commandId: 'Workspace.CloseWorkflow',
|
||||
combo: {
|
||||
key: 'w',
|
||||
ctrl: true
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
aboutPageBadges: [
|
||||
{
|
||||
label: 'ComfyUI_desktop v' + desktopAppVersion,
|
||||
|
||||
@@ -6,7 +6,6 @@ import './electronAdapter'
|
||||
import './groupNode'
|
||||
import './groupNodeManage'
|
||||
import './groupOptions'
|
||||
import './invertMenuScrolling'
|
||||
import './load3d'
|
||||
import './maskeditor'
|
||||
import './nodeTemplates'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -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'
|
||||
@@ -13,6 +14,30 @@ import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
async function uploadTempImage(imageData, prefix) {
|
||||
const blob = await fetch(imageData).then((r) => r.blob())
|
||||
const name = `${prefix}_${Date.now()}.png`
|
||||
const file = new File([blob], name)
|
||||
|
||||
const body = new FormData()
|
||||
body.append('image', file)
|
||||
body.append('subfolder', 'threed')
|
||||
body.append('type', 'temp')
|
||||
|
||||
const resp = await api.fetchApi('/upload/image', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
|
||||
if (resp.status !== 200) {
|
||||
const err = `Error uploading temp image: ${resp.status} - ${resp.statusText}`
|
||||
useToastStore().addAlert(err)
|
||||
throw new Error(err)
|
||||
}
|
||||
|
||||
return await resp.json()
|
||||
}
|
||||
|
||||
async function uploadFile(
|
||||
load3d: Load3d,
|
||||
file: File,
|
||||
@@ -91,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
|
||||
@@ -107,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()
|
||||
@@ -130,9 +158,10 @@ class Load3d {
|
||||
this.perspectiveCamera.lookAt(0, 0, 0)
|
||||
this.orthographicCamera.lookAt(0, 0, 0)
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true })
|
||||
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
|
||||
|
||||
@@ -179,13 +208,151 @@ 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
|
||||
this.perspectiveCamera.updateProjectionMatrix()
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
}
|
||||
}
|
||||
|
||||
getCameraState() {
|
||||
const currentType = this.getCurrentCameraType()
|
||||
return {
|
||||
@@ -433,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()
|
||||
}
|
||||
|
||||
@@ -469,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()
|
||||
}
|
||||
@@ -556,6 +738,7 @@ class Load3d {
|
||||
}
|
||||
|
||||
this.controls.dispose()
|
||||
this.viewHelper.dispose()
|
||||
this.renderer.dispose()
|
||||
this.renderer.domElement.remove()
|
||||
this.scene.clear()
|
||||
@@ -758,11 +941,18 @@ class Load3d {
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
}
|
||||
|
||||
captureScene(width: number, height: number): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
captureScene(
|
||||
width: number,
|
||||
height: number
|
||||
): Promise<{ scene: string; mask: string }> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const originalWidth = this.renderer.domElement.width
|
||||
const originalHeight = this.renderer.domElement.height
|
||||
const originalClearColor = this.renderer.getClearColor(
|
||||
new THREE.Color()
|
||||
)
|
||||
const originalClearAlpha = this.renderer.getClearAlpha()
|
||||
|
||||
this.renderer.setSize(width, height)
|
||||
|
||||
@@ -779,14 +969,20 @@ class Load3d {
|
||||
this.orthographicCamera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
this.renderer.clear()
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
const sceneData = this.renderer.domElement.toDataURL('image/png')
|
||||
|
||||
const imageData = this.renderer.domElement.toDataURL('image/png')
|
||||
this.renderer.setClearColor(0x000000, 0)
|
||||
this.renderer.clear()
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
const maskData = this.renderer.domElement.toDataURL('image/png')
|
||||
|
||||
this.renderer.setClearColor(originalClearColor, originalClearAlpha)
|
||||
this.renderer.setSize(originalWidth, originalHeight)
|
||||
this.handleResize()
|
||||
|
||||
resolve(imageData)
|
||||
resolve({ scene: sceneData, mask: maskData })
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
@@ -803,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)
|
||||
@@ -977,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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1033,13 +1203,11 @@ function configureLoad3D(
|
||||
load3d: Load3d,
|
||||
loadFolder: 'input' | 'output',
|
||||
modelWidget: IWidget,
|
||||
showGrid: IWidget,
|
||||
cameraType: IWidget,
|
||||
view: IWidget,
|
||||
material: IWidget,
|
||||
bgColor: IWidget,
|
||||
lightIntensity: IWidget,
|
||||
upDirection: IWidget,
|
||||
fov: IWidget,
|
||||
cameraState?: any,
|
||||
postModelUpdateFunc?: (load3d: Load3d) => void
|
||||
) {
|
||||
@@ -1094,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)
|
||||
}
|
||||
@@ -1137,6 +1289,12 @@ function configureLoad3D(
|
||||
load3d.setUpDirection(
|
||||
upDirection.value as 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z'
|
||||
)
|
||||
|
||||
fov.callback = (value: number) => {
|
||||
load3d.setFOV(value)
|
||||
}
|
||||
|
||||
load3d.setFOV(fov.value as number)
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
@@ -1262,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')
|
||||
@@ -1282,6 +1432,8 @@ app.registerExtension({
|
||||
(w: IWidget) => w.name === 'up_direction'
|
||||
)
|
||||
|
||||
const fov = node.widgets.find((w: IWidget) => w.name === 'fov')
|
||||
|
||||
let cameraState
|
||||
try {
|
||||
const cameraInfo = node.properties['Camera Info']
|
||||
@@ -1301,13 +1453,11 @@ app.registerExtension({
|
||||
load3d,
|
||||
'input',
|
||||
modelWidget,
|
||||
showGrid,
|
||||
cameraType,
|
||||
view,
|
||||
material,
|
||||
bgColor,
|
||||
lightIntensity,
|
||||
upDirection,
|
||||
fov,
|
||||
cameraState
|
||||
)
|
||||
|
||||
@@ -1318,30 +1468,20 @@ app.registerExtension({
|
||||
sceneWidget.serializeValue = async () => {
|
||||
node.properties['Camera Info'] = JSON.stringify(load3d.getCameraState())
|
||||
|
||||
const imageData = await load3d.captureScene(w.value, h.value)
|
||||
const { scene: imageData, mask: maskData } = await load3d.captureScene(
|
||||
w.value,
|
||||
h.value
|
||||
)
|
||||
|
||||
const blob = await fetch(imageData).then((r) => r.blob())
|
||||
const name = `scene_${Date.now()}.png`
|
||||
const file = new File([blob], name)
|
||||
const [data, dataMask] = await Promise.all([
|
||||
uploadTempImage(imageData, 'scene'),
|
||||
uploadTempImage(maskData, 'scene_mask')
|
||||
])
|
||||
|
||||
const body = new FormData()
|
||||
body.append('image', file)
|
||||
body.append('subfolder', 'threed')
|
||||
body.append('type', 'temp')
|
||||
|
||||
const resp = await api.fetchApi('/upload/image', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
|
||||
if (resp.status !== 200) {
|
||||
const err = `Error uploading scene capture: ${resp.status} - ${resp.statusText}`
|
||||
useToastStore().addAlert(err)
|
||||
throw new Error(err)
|
||||
return {
|
||||
image: `threed/${data.name} [temp]`,
|
||||
mask: `threed/${dataMask.name} [temp]`
|
||||
}
|
||||
|
||||
const data = await resp.json()
|
||||
return `threed/${data.name} [temp]`
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1526,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')
|
||||
@@ -1557,6 +1689,8 @@ app.registerExtension({
|
||||
}
|
||||
}
|
||||
|
||||
const fov = node.widgets.find((w: IWidget) => w.name === 'fov')
|
||||
|
||||
let cameraState
|
||||
try {
|
||||
const cameraInfo = node.properties['Camera Info']
|
||||
@@ -1576,13 +1710,11 @@ app.registerExtension({
|
||||
load3d,
|
||||
'input',
|
||||
modelWidget,
|
||||
showGrid,
|
||||
cameraType,
|
||||
view,
|
||||
material,
|
||||
bgColor,
|
||||
lightIntensity,
|
||||
upDirection,
|
||||
fov,
|
||||
cameraState,
|
||||
(load3d: Load3d) => {
|
||||
const animationLoad3d = load3d as Load3dAnimation
|
||||
@@ -1608,30 +1740,20 @@ app.registerExtension({
|
||||
|
||||
load3d.toggleAnimation(false)
|
||||
|
||||
const imageData = await load3d.captureScene(w.value, h.value)
|
||||
const { scene: imageData, mask: maskData } = await load3d.captureScene(
|
||||
w.value,
|
||||
h.value
|
||||
)
|
||||
|
||||
const blob = await fetch(imageData).then((r) => r.blob())
|
||||
const name = `scene_${Date.now()}.png`
|
||||
const file = new File([blob], name)
|
||||
const [data, dataMask] = await Promise.all([
|
||||
uploadTempImage(imageData, 'scene'),
|
||||
uploadTempImage(maskData, 'scene_mask')
|
||||
])
|
||||
|
||||
const body = new FormData()
|
||||
body.append('image', file)
|
||||
body.append('subfolder', 'threed')
|
||||
body.append('type', 'temp')
|
||||
|
||||
const resp = await api.fetchApi('/upload/image', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
|
||||
if (resp.status !== 200) {
|
||||
const err = `Error uploading scene capture: ${resp.status} - ${resp.statusText}`
|
||||
useToastStore().addAlert(err)
|
||||
throw new Error(err)
|
||||
return {
|
||||
image: `threed/${data.name} [temp]`,
|
||||
mask: `threed/${dataMask.name} [temp]`
|
||||
}
|
||||
|
||||
const data = await resp.json()
|
||||
return `threed/${data.name} [temp]`
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1724,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')
|
||||
@@ -1744,6 +1858,8 @@ app.registerExtension({
|
||||
(w: IWidget) => w.name === 'up_direction'
|
||||
)
|
||||
|
||||
const fov = node.widgets.find((w: IWidget) => w.name === 'fov')
|
||||
|
||||
const onExecuted = node.onExecuted
|
||||
|
||||
node.onExecuted = function (message: any) {
|
||||
@@ -1765,13 +1881,11 @@ app.registerExtension({
|
||||
load3d,
|
||||
'output',
|
||||
modelWidget,
|
||||
showGrid,
|
||||
cameraType,
|
||||
view,
|
||||
material,
|
||||
bgColor,
|
||||
lightIntensity,
|
||||
upDirection
|
||||
upDirection,
|
||||
fov
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
import { api } from '../../scripts/api'
|
||||
import { app } from '../../scripts/app'
|
||||
import { ComfyApp } from '../../scripts/app'
|
||||
import { $el, ComfyDialog } from '../../scripts/ui'
|
||||
import { getStorageValue, setStorageValue } from '../../scripts/utils'
|
||||
import { ClipspaceDialog } from './clipspace'
|
||||
import { MaskEditorDialogOld } from './maskEditorOld'
|
||||
|
||||
@@ -262,15 +265,15 @@ var styles = `
|
||||
}
|
||||
#maskEditor_toolPanel {
|
||||
height: 100%;
|
||||
width: var(--sidebar-width);
|
||||
width: 4rem;
|
||||
z-index: 8888;
|
||||
background: var(--comfy-menu-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.maskEditor_toolPanelContainer {
|
||||
width: var(--sidebar-width);
|
||||
height: var(--sidebar-width);
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -331,7 +334,7 @@ var styles = `
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
#maskEditor_pointerZone {
|
||||
width: calc(100% - var(--sidebar-width) - 220px);
|
||||
width: calc(100% - 4rem - 220px);
|
||||
height: 100%;
|
||||
}
|
||||
#maskEditor_uiContainer {
|
||||
@@ -703,8 +706,8 @@ var styles = `
|
||||
}
|
||||
|
||||
.maskEditor_toolPanelZoomIndicator {
|
||||
width: var(--sidebar-width);
|
||||
height: var(--sidebar-width);
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
@@ -776,10 +779,37 @@ interface Offset {
|
||||
}
|
||||
|
||||
export interface Brush {
|
||||
type: BrushShape
|
||||
size: number
|
||||
opacity: number
|
||||
hardness: number
|
||||
type: BrushShape
|
||||
smoothingPrecision: number
|
||||
}
|
||||
|
||||
const saveBrushToCache = debounce(function (key: string, brush: Brush): void {
|
||||
try {
|
||||
const brushString = JSON.stringify(brush)
|
||||
setStorageValue(key, brushString)
|
||||
} catch (error) {
|
||||
console.error('Failed to save brush to cache:', error)
|
||||
}
|
||||
}, 300)
|
||||
|
||||
function loadBrushFromCache(key: string): Brush | null {
|
||||
try {
|
||||
const brushString = getStorageValue(key)
|
||||
if (brushString) {
|
||||
const brush = JSON.parse(brushString) as Brush
|
||||
console.log('Loaded brush from cache:', brush)
|
||||
return brush
|
||||
} else {
|
||||
console.log('No brush found in cache.')
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load brush from cache:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
type Callback = (data?: any) => void
|
||||
@@ -1125,7 +1155,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) => {
|
||||
@@ -1952,12 +1982,19 @@ class BrushTool {
|
||||
'Comfy.MaskEditor.BrushAdjustmentSpeed'
|
||||
)
|
||||
|
||||
this.brushSettings = {
|
||||
size: 10,
|
||||
opacity: 100,
|
||||
hardness: 1,
|
||||
type: BrushShape.Arc
|
||||
const cachedBrushSettings = loadBrushFromCache('maskeditor_brush_settings')
|
||||
if (cachedBrushSettings) {
|
||||
this.brushSettings = cachedBrushSettings
|
||||
} else {
|
||||
this.brushSettings = {
|
||||
type: BrushShape.Arc,
|
||||
size: 10,
|
||||
opacity: 0.7,
|
||||
hardness: 1,
|
||||
smoothingPrecision: 10
|
||||
}
|
||||
}
|
||||
|
||||
this.maskBlendMode = MaskBlendMode.Black
|
||||
}
|
||||
|
||||
@@ -2016,6 +2053,10 @@ class BrushTool {
|
||||
'brushType',
|
||||
async () => this.brushSettings.type
|
||||
)
|
||||
this.messageBroker.createPullTopic(
|
||||
'brushSmoothingPrecision',
|
||||
async () => this.brushSettings.smoothingPrecision
|
||||
)
|
||||
this.messageBroker.createPullTopic(
|
||||
'maskBlendMode',
|
||||
async () => this.maskBlendMode
|
||||
@@ -2143,7 +2184,7 @@ class BrushTool {
|
||||
}
|
||||
|
||||
const distanceBetweenPoints =
|
||||
(this.brushSettings.size / this.smoothingPrecision) * 6
|
||||
(this.brushSettings.size / this.brushSettings.smoothingPrecision) * 6
|
||||
const stepNr = Math.ceil(totalLength / distanceBetweenPoints)
|
||||
|
||||
let interpolatedPoints = points
|
||||
@@ -2190,7 +2231,7 @@ class BrushTool {
|
||||
const brush_size = await this.messageBroker.pull('brushSize')
|
||||
const distance = Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2)
|
||||
const steps = Math.ceil(
|
||||
distance / ((brush_size / this.smoothingPrecision) * 4)
|
||||
distance / ((brush_size / this.brushSettings.smoothingPrecision) * 4)
|
||||
) // Adjust for smoother lines
|
||||
const interpolatedOpacity =
|
||||
1 / (1 + Math.exp(-6 * (this.brushSettings.opacity - 0.5))) -
|
||||
@@ -2545,23 +2586,27 @@ class BrushTool {
|
||||
|
||||
private setBrushSize(size: number) {
|
||||
this.brushSettings.size = size
|
||||
saveBrushToCache('maskeditor_brush_settings', this.brushSettings)
|
||||
}
|
||||
|
||||
private setBrushOpacity(opacity: number) {
|
||||
this.brushSettings.opacity = opacity
|
||||
saveBrushToCache('maskeditor_brush_settings', this.brushSettings)
|
||||
}
|
||||
|
||||
private setBrushHardness(hardness: number) {
|
||||
this.brushSettings.hardness = hardness
|
||||
saveBrushToCache('maskeditor_brush_settings', this.brushSettings)
|
||||
}
|
||||
|
||||
private setBrushType(type: BrushShape) {
|
||||
this.brushSettings.type = type
|
||||
saveBrushToCache('maskeditor_brush_settings', this.brushSettings)
|
||||
}
|
||||
|
||||
private setBrushSmoothingPrecision(precision: number) {
|
||||
//console.log('precision', precision)
|
||||
this.smoothingPrecision = precision
|
||||
this.brushSettings.smoothingPrecision = precision
|
||||
saveBrushToCache('maskeditor_brush_settings', this.brushSettings)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2825,7 +2870,6 @@ class UIManager {
|
||||
const circle_shape = document.createElement('div')
|
||||
circle_shape.id = 'maskEditor_sidePanelBrushShapeCircle'
|
||||
circle_shape.classList.add(shapeColor)
|
||||
circle_shape.style.background = 'var(--p-button-text-primary-color)'
|
||||
circle_shape.addEventListener('click', () => {
|
||||
this.messageBroker.publish('setBrushShape', BrushShape.Arc)
|
||||
this.setBrushBorderRadius()
|
||||
@@ -2836,7 +2880,6 @@ class UIManager {
|
||||
const square_shape = document.createElement('div')
|
||||
square_shape.id = 'maskEditor_sidePanelBrushShapeSquare'
|
||||
square_shape.classList.add(shapeColor)
|
||||
square_shape.style.background = ''
|
||||
square_shape.addEventListener('click', () => {
|
||||
this.messageBroker.publish('setBrushShape', BrushShape.Rect)
|
||||
this.setBrushBorderRadius()
|
||||
@@ -2844,6 +2887,16 @@ class UIManager {
|
||||
circle_shape.style.background = ''
|
||||
})
|
||||
|
||||
if (
|
||||
(await this.messageBroker.pull('brushSettings')).type === BrushShape.Arc
|
||||
) {
|
||||
circle_shape.style.background = 'var(--p-button-text-primary-color)'
|
||||
square_shape.style.background = ''
|
||||
} else {
|
||||
circle_shape.style.background = ''
|
||||
square_shape.style.background = 'var(--p-button-text-primary-color)'
|
||||
}
|
||||
|
||||
brush_shape_container.appendChild(circle_shape)
|
||||
brush_shape_container.appendChild(square_shape)
|
||||
|
||||
@@ -2855,7 +2908,7 @@ class UIManager {
|
||||
1,
|
||||
100,
|
||||
1,
|
||||
10,
|
||||
(await this.messageBroker.pull('brushSettings')).size,
|
||||
(event, value) => {
|
||||
this.messageBroker.publish('setBrushSize', parseInt(value))
|
||||
this.updateBrushPreview()
|
||||
@@ -2868,7 +2921,7 @@ class UIManager {
|
||||
0,
|
||||
1,
|
||||
0.01,
|
||||
0.7,
|
||||
(await this.messageBroker.pull('brushSettings')).opacity,
|
||||
(event, value) => {
|
||||
this.messageBroker.publish('setBrushOpacity', parseFloat(value))
|
||||
this.updateBrushPreview()
|
||||
@@ -2881,7 +2934,7 @@ class UIManager {
|
||||
0,
|
||||
1,
|
||||
0.01,
|
||||
1,
|
||||
(await this.messageBroker.pull('brushSettings')).hardness,
|
||||
(event, value) => {
|
||||
this.messageBroker.publish('setBrushHardness', parseFloat(value))
|
||||
this.updateBrushPreview()
|
||||
@@ -2894,7 +2947,7 @@ class UIManager {
|
||||
1,
|
||||
100,
|
||||
1,
|
||||
10,
|
||||
(await this.messageBroker.pull('brushSettings')).smoothingPrecision,
|
||||
(event, value) => {
|
||||
this.messageBroker.publish(
|
||||
'setBrushSmoothingPrecision',
|
||||
@@ -2903,7 +2956,31 @@ class UIManager {
|
||||
}
|
||||
)
|
||||
|
||||
const resetBrushSettingsButton = document.createElement('button')
|
||||
resetBrushSettingsButton.id = 'resetBrushSettingsButton'
|
||||
resetBrushSettingsButton.innerText = 'Reset to Default'
|
||||
|
||||
resetBrushSettingsButton.addEventListener('click', () => {
|
||||
this.messageBroker.publish('setBrushShape', BrushShape.Arc)
|
||||
this.messageBroker.publish('setBrushSize', 10)
|
||||
this.messageBroker.publish('setBrushOpacity', 0.7)
|
||||
this.messageBroker.publish('setBrushHardness', 1)
|
||||
this.messageBroker.publish('setBrushSmoothingPrecision', 10)
|
||||
|
||||
circle_shape.style.background = 'var(--p-button-text-primary-color)'
|
||||
square_shape.style.background = ''
|
||||
|
||||
thicknesSliderObj.slider.value = '10'
|
||||
opacitySliderObj.slider.value = '0.7'
|
||||
hardnessSliderObj.slider.value = '1'
|
||||
brushSmoothingPrecisionSliderObj.slider.value = '10'
|
||||
|
||||
this.setBrushBorderRadius()
|
||||
this.updateBrushPreview()
|
||||
})
|
||||
|
||||
brush_settings_container.appendChild(brush_settings_title)
|
||||
brush_settings_container.appendChild(resetBrushSettingsButton)
|
||||
brush_settings_container.appendChild(brush_shape_outer_container)
|
||||
brush_settings_container.appendChild(thicknesSliderObj.container)
|
||||
brush_settings_container.appendChild(opacitySliderObj.container)
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -633,7 +633,8 @@ export function mergeIfValid(
|
||||
k !== 'defaultInput' &&
|
||||
k !== 'control_after_generate' &&
|
||||
k !== 'multiline' &&
|
||||
k !== 'tooltip'
|
||||
k !== 'tooltip' &&
|
||||
k !== 'dynamicPrompts'
|
||||
) {
|
||||
let v1 = config1[1][k]
|
||||
let v2 = config2[1]?.[k]
|
||||
|
||||
@@ -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
|
||||
})
|
||||
@@ -33,12 +36,21 @@ export function useTerminal(element: Ref<HTMLElement>) {
|
||||
|
||||
return {
|
||||
terminal,
|
||||
useAutoSize(
|
||||
root: Ref<HTMLElement>,
|
||||
autoRows: boolean = true,
|
||||
autoCols: boolean = true,
|
||||
useAutoSize({
|
||||
root,
|
||||
autoRows = true,
|
||||
autoCols = true,
|
||||
minCols = Number.NEGATIVE_INFINITY,
|
||||
minRows = Number.NEGATIVE_INFINITY,
|
||||
onResize
|
||||
}: {
|
||||
root: Ref<HTMLElement>
|
||||
autoRows?: boolean
|
||||
autoCols?: boolean
|
||||
minCols?: number
|
||||
minRows?: number
|
||||
onResize?: () => void
|
||||
) {
|
||||
}) {
|
||||
const ensureValidRows = (rows: number | undefined) => {
|
||||
if (rows == null || isNaN(rows)) {
|
||||
return root.value?.clientHeight / 20
|
||||
@@ -58,8 +70,14 @@ export function useTerminal(element: Ref<HTMLElement>) {
|
||||
const dims = fitAddon.proposeDimensions()
|
||||
// Sometimes propose returns NaN, so we may need to estimate.
|
||||
terminal.resize(
|
||||
autoCols ? ensureValidCols(dims?.cols) : terminal.cols,
|
||||
autoRows ? ensureValidRows(dims?.rows) : terminal.rows
|
||||
Math.max(
|
||||
autoCols ? ensureValidCols(dims?.cols) : terminal.cols,
|
||||
minCols
|
||||
),
|
||||
Math.max(
|
||||
autoRows ? ensureValidRows(dims?.rows) : terminal.rows,
|
||||
minRows
|
||||
)
|
||||
)
|
||||
onResize?.()
|
||||
}
|
||||
|
||||
@@ -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,13 +20,16 @@ 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'
|
||||
|
||||
export function useCoreCommands(): ComfyCommand[] {
|
||||
const workflowService = useWorkflowService()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const dialogService = useDialogService()
|
||||
const getTracker = () => useWorkflowStore()?.activeWorkflow?.changeTracker
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const getTracker = () => workflowStore.activeWorkflow?.changeTracker
|
||||
|
||||
const getSelectedNodes = (): LGraphNode[] => {
|
||||
const selectedNodes = app.canvas.selected_nodes
|
||||
@@ -409,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)
|
||||
}
|
||||
}
|
||||
})()
|
||||
@@ -512,6 +519,25 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
function: () => {
|
||||
dialogService.showSettingsDialog('about')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.DuplicateWorkflow',
|
||||
icon: 'pi pi-clone',
|
||||
label: 'Duplicate Current Workflow',
|
||||
versionAdded: '1.6.15',
|
||||
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)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -74,6 +74,9 @@
|
||||
"Comfy_ClearWorkflow": {
|
||||
"label": "Clear Workflow"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "Duplicate Current Workflow"
|
||||
},
|
||||
"Comfy_ExportWorkflow": {
|
||||
"label": "Export Workflow"
|
||||
},
|
||||
@@ -149,6 +152,9 @@
|
||||
"Comfy_Undo": {
|
||||
"label": "Undo"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "Close Current Workflow"
|
||||
},
|
||||
"Workspace_NextOpenedWorkflow": {
|
||||
"label": "Next Opened Workflow"
|
||||
},
|
||||
|
||||
@@ -68,7 +68,21 @@
|
||||
"disableAll": "Disable All",
|
||||
"command": "Command",
|
||||
"keybinding": "Keybinding",
|
||||
"upload": "Upload"
|
||||
"upload": "Upload",
|
||||
"export": "Export",
|
||||
"workflow": "Workflow",
|
||||
"success": "Success",
|
||||
"ok": "OK"
|
||||
},
|
||||
"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",
|
||||
@@ -139,7 +153,7 @@
|
||||
"appDataLocationTooltip": "ComfyUI's app data directory. Stores:\n- Logs\n- Server configs",
|
||||
"appPathLocationTooltip": "ComfyUI's app asset directory. Stores the ComfyUI code and assets",
|
||||
"migrateFromExistingInstallation": "Migrate from Existing Installation",
|
||||
"migrationSourcePathDescription": "If you have an existing ComfyUI installation, we can copy/link your existing user files and models to the new installation.",
|
||||
"migrationSourcePathDescription": "If you have an existing ComfyUI installation, we can copy/link your existing user files and models to the new installation. Your existing ComfyUI installation will not be affected.",
|
||||
"selectItemsToMigrate": "Select Items to Migrate",
|
||||
"migrationOptional": "Migration is optional. If you don't have an existing installation, you can skip this step.",
|
||||
"desktopAppSettings": "Desktop App Settings",
|
||||
@@ -169,25 +183,38 @@
|
||||
},
|
||||
"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",
|
||||
"errorUpdatingConsent": "Error Updating Consent",
|
||||
"errorUpdatingConsentDetail": "Failed to update metrics consent settings",
|
||||
"autoUpdateDescription": "Automatically download updates when they become available. You will be notified before updates are installed.",
|
||||
"allowMetricsDescription": "Help improve ComfyUI by sending anonymous usage metrics. No personal information or workflow content will be collected.",
|
||||
"learnMoreAboutData": "Learn more about data collection",
|
||||
"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"
|
||||
},
|
||||
"doNotCollect": {
|
||||
"personalInformation": "Personal information",
|
||||
"fileSystemInformation": "File system information",
|
||||
"workflowContents": "Workflow contents",
|
||||
"customNodeConfigurations": "Custom node configurations"
|
||||
},
|
||||
"viewFullPolicy": "View full policy"
|
||||
}
|
||||
},
|
||||
"customNodes": "Custom Nodes",
|
||||
"customNodesDescription": "Reinstall custom nodes from existing ComfyUI installations."
|
||||
"customNodesDescription": "Reinstall custom nodes from existing ComfyUI installations.",
|
||||
"helpImprove": "Please help improve ComfyUI",
|
||||
"moreInfo": "For more info, please read our",
|
||||
"privacyPolicy": "privacy policy",
|
||||
"metricsEnabled": "Metrics Enabled",
|
||||
"metricsDisabled": "Metrics Disabled",
|
||||
"updateConsent": "You previously opted in to reporting crashes. We are now tracking event-based metrics to help identify bugs and improve the app. No personal identifiable information is collected."
|
||||
},
|
||||
"serverStart": {
|
||||
"reinstall": "Reinstall",
|
||||
@@ -278,7 +305,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",
|
||||
@@ -348,6 +377,7 @@
|
||||
"Zoom Out": "Zoom Out",
|
||||
"Clear Pending Tasks": "Clear Pending Tasks",
|
||||
"Clear Workflow": "Clear Workflow",
|
||||
"Duplicate Current Workflow": "Duplicate Current Workflow",
|
||||
"Export": "Export",
|
||||
"Export (API)": "Export (API)",
|
||||
"Fit Group To Contents": "Fit Group To Contents",
|
||||
@@ -373,6 +403,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",
|
||||
@@ -579,6 +610,7 @@
|
||||
"combine": "combine",
|
||||
"cond single": "cond single",
|
||||
"controlnet": "controlnet",
|
||||
"inpaint": "inpaint",
|
||||
"scheduling": "scheduling",
|
||||
"create": "create",
|
||||
"mask": "mask",
|
||||
@@ -598,7 +630,6 @@
|
||||
"batch": "batch",
|
||||
"video_models": "video_models",
|
||||
"upscaling": "upscaling",
|
||||
"inpaint": "inpaint",
|
||||
"instructpix2pix": "instructpix2pix",
|
||||
"compositing": "compositing",
|
||||
"samplers": "samplers",
|
||||
|
||||
@@ -177,13 +177,16 @@
|
||||
},
|
||||
"CLIPLoader": {
|
||||
"display_name": "Load CLIP",
|
||||
"description": "[Recipes]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 / clip-g / clip-l\nstable_audio: t5\nmochi: t5",
|
||||
"description": "[Recipes]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 / clip-g / clip-l\nstable_audio: t5\nmochi: t5\ncosmos: old t5 xxl",
|
||||
"inputs": {
|
||||
"clip_name": {
|
||||
"name": "clip_name"
|
||||
},
|
||||
"type": {
|
||||
"name": "type"
|
||||
},
|
||||
"device": {
|
||||
"name": "device"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -859,6 +862,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CosmosImageToVideoLatent": {
|
||||
"display_name": "CosmosImageToVideoLatent",
|
||||
"inputs": {
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
},
|
||||
"width": {
|
||||
"name": "width"
|
||||
},
|
||||
"height": {
|
||||
"name": "height"
|
||||
},
|
||||
"length": {
|
||||
"name": "length"
|
||||
},
|
||||
"batch_size": {
|
||||
"name": "batch_size"
|
||||
},
|
||||
"start_image": {
|
||||
"name": "start_image"
|
||||
},
|
||||
"end_image": {
|
||||
"name": "end_image"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateHookKeyframe": {
|
||||
"display_name": "Create Hook Keyframe",
|
||||
"inputs": {
|
||||
@@ -1100,6 +1129,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 +1162,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 +1250,26 @@
|
||||
},
|
||||
"type": {
|
||||
"name": "type"
|
||||
},
|
||||
"device": {
|
||||
"name": "device"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmptyCosmosLatentVideo": {
|
||||
"display_name": "EmptyCosmosLatentVideo",
|
||||
"inputs": {
|
||||
"width": {
|
||||
"name": "width"
|
||||
},
|
||||
"height": {
|
||||
"name": "height"
|
||||
},
|
||||
"length": {
|
||||
"name": "length"
|
||||
},
|
||||
"batch_size": {
|
||||
"name": "batch_size"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2293,6 +2366,9 @@
|
||||
},
|
||||
"up_direction": {
|
||||
"name": "up_direction"
|
||||
},
|
||||
"fov": {
|
||||
"name": "fov"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -2345,6 +2421,9 @@
|
||||
},
|
||||
"animation_speed": {
|
||||
"name": "animation_speed"
|
||||
},
|
||||
"fov": {
|
||||
"name": "fov"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -4249,6 +4328,9 @@
|
||||
},
|
||||
"up_direction": {
|
||||
"name": "up_direction"
|
||||
},
|
||||
"fov": {
|
||||
"name": "fov"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4706,6 +4788,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SetFirstSigma": {
|
||||
"display_name": "SetFirstSigma",
|
||||
"inputs": {
|
||||
"sigmas": {
|
||||
"name": "sigmas"
|
||||
},
|
||||
"sigma": {
|
||||
"name": "sigma"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SetHookKeyframes": {
|
||||
"display_name": "Set Hook Keyframes",
|
||||
"inputs": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -74,6 +74,9 @@
|
||||
"Comfy_ClearWorkflow": {
|
||||
"label": "Effacer le flux de travail"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "Dupliquer le flux de travail actuel"
|
||||
},
|
||||
"Comfy_ExportWorkflow": {
|
||||
"label": "Exporter le flux de travail"
|
||||
},
|
||||
@@ -149,6 +152,9 @@
|
||||
"Comfy_Undo": {
|
||||
"label": "Annuler"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "Fermer le flux de travail actuel"
|
||||
},
|
||||
"Workspace_NextOpenedWorkflow": {
|
||||
"label": "Flux de travail ouvert suivant"
|
||||
},
|
||||
|
||||
@@ -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.",
|
||||
@@ -106,6 +107,7 @@
|
||||
"noTasksFound": "Aucune tâche trouvée",
|
||||
"noTasksFoundMessage": "Il n'y a pas de tâches dans la file d'attente.",
|
||||
"noWorkflowsFound": "Aucun flux de travail trouvé.",
|
||||
"ok": "OK",
|
||||
"openNewIssue": "Ouvrir un nouveau problème",
|
||||
"overwrite": "Écraser",
|
||||
"reconnected": "Reconnecté",
|
||||
@@ -128,10 +130,12 @@
|
||||
"searchWorkflows": "Rechercher des flux de travail",
|
||||
"settings": "Paramètres",
|
||||
"showReport": "Afficher le rapport",
|
||||
"success": "Succès",
|
||||
"systemInfo": "Informations système",
|
||||
"terminal": "Terminal",
|
||||
"upload": "Téléverser",
|
||||
"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",
|
||||
@@ -184,6 +188,7 @@
|
||||
"selectGpu": "Sélectionnez le GPU",
|
||||
"selectGpuDescription": "Sélectionnez le type de GPU que vous avez"
|
||||
},
|
||||
"helpImprove": "Veuillez aider à améliorer ComfyUI",
|
||||
"installLocation": "Emplacement d'installation",
|
||||
"installLocationDescription": "Sélectionnez le répertoire pour les données utilisateur de ComfyUI. Un environnement python sera installé à l'emplacement sélectionné. Veuillez vous assurer que le disque sélectionné a suffisamment d'espace (~15GB) restant.",
|
||||
"installLocationTooltip": "Répertoire des données utilisateur de ComfyUI. Stocke :\n- Environnement Python\n- Modèles\n- Nœuds personnalisés\n",
|
||||
@@ -195,35 +200,57 @@
|
||||
"title": "Configuration manuelle",
|
||||
"virtualEnvironmentPath": "Chemin de l'environnement virtuel"
|
||||
},
|
||||
"metricsDisabled": "Métriques désactivées",
|
||||
"metricsEnabled": "Métriques activées",
|
||||
"migrateFromExistingInstallation": "Migrer à partir d'une installation existante",
|
||||
"migration": "Migration",
|
||||
"migrationOptional": "La migration est facultative. Si vous n'avez pas d'installation existante, vous pouvez sauter cette étape.",
|
||||
"migrationSourcePathDescription": "Si vous avez une installation existante de ComfyUI, nous pouvons copier/lier vos fichiers utilisateur et modèles existants à la nouvelle installation.",
|
||||
"migrationSourcePathDescription": "Si vous avez une installation existante de ComfyUI, nous pouvons copier/lier vos fichiers utilisateur et modèles existants à la nouvelle installation. Votre installation existante de ComfyUI ne sera pas affectée.",
|
||||
"moreInfo": "Pour plus d'informations, veuillez lire notre",
|
||||
"parentMissing": "Le chemin n'existe pas - créez d'abord le répertoire contenant",
|
||||
"pathExists": "Le répertoire existe déjà - veuillez vous assurer que vous avez sauvegardé toutes les données",
|
||||
"pathValidationFailed": "Échec de la validation du chemin",
|
||||
"privacyPolicy": "politique de confidentialité",
|
||||
"selectItemsToMigrate": "Sélectionnez les éléments à migrer",
|
||||
"settings": {
|
||||
"allowMetrics": "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"
|
||||
},
|
||||
"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",
|
||||
"viewFullPolicy": "Voir la politique complète",
|
||||
"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 :"
|
||||
},
|
||||
"errorUpdatingConsent": "Erreur de mise à jour du consentement",
|
||||
"errorUpdatingConsentDetail": "Échec de la mise à jour des paramètres de consentement aux métriques",
|
||||
"learnMoreAboutData": "En savoir plus sur la collecte de données"
|
||||
},
|
||||
"systemLocations": "Emplacements système",
|
||||
"unhandledError": "Erreur inconnue"
|
||||
"unhandledError": "Erreur inconnue",
|
||||
"updateConsent": "Vous avez précédemment accepté de signaler les plantages. Nous suivons maintenant des métriques basées sur les événements pour aider à identifier les bugs et améliorer l'application. Aucune information personnelle identifiable n'est collectée."
|
||||
},
|
||||
"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",
|
||||
@@ -257,12 +284,14 @@
|
||||
"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",
|
||||
"ComfyUI Issues": "Problèmes de ComfyUI",
|
||||
"Convert selected nodes to group node": "Convertir les nœuds sélectionnés en nœud de groupe",
|
||||
"Desktop User Guide": "Guide de l'utilisateur de bureau",
|
||||
"Duplicate Current Workflow": "Dupliquer le flux de travail actuel",
|
||||
"Edit": "Éditer",
|
||||
"Export": "Exporter",
|
||||
"Export (API)": "Exporter (API)",
|
||||
@@ -620,11 +649,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": {
|
||||
|
||||
@@ -113,12 +113,15 @@
|
||||
}
|
||||
},
|
||||
"CLIPLoader": {
|
||||
"description": "[Recettes]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 / clip-g / clip-l\nstable_audio: t5\nmochi: t5",
|
||||
"description": "[Recettes]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 / clip-g / clip-l\nstable_audio: t5\nmochi: t5\ncosmos: old t5 xxl",
|
||||
"display_name": "Charger CLIP",
|
||||
"inputs": {
|
||||
"clip_name": {
|
||||
"name": "clip_name"
|
||||
},
|
||||
"device": {
|
||||
"name": "appareil"
|
||||
},
|
||||
"type": {
|
||||
"name": "type"
|
||||
}
|
||||
@@ -859,6 +862,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CosmosImageToVideoLatent": {
|
||||
"display_name": "CosmosImageVersVidéoLatent",
|
||||
"inputs": {
|
||||
"batch_size": {
|
||||
"name": "taille_du_lot"
|
||||
},
|
||||
"end_image": {
|
||||
"name": "image_de_fin"
|
||||
},
|
||||
"height": {
|
||||
"name": "hauteur"
|
||||
},
|
||||
"length": {
|
||||
"name": "longueur"
|
||||
},
|
||||
"start_image": {
|
||||
"name": "image_de_départ"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
},
|
||||
"width": {
|
||||
"name": "largeur"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateHookKeyframe": {
|
||||
"display_name": "Créer une image clé de crochet",
|
||||
"inputs": {
|
||||
@@ -1100,6 +1129,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 +1162,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,11 +1248,31 @@
|
||||
"clip_name2": {
|
||||
"name": "nom_clip2"
|
||||
},
|
||||
"device": {
|
||||
"name": "appareil"
|
||||
},
|
||||
"type": {
|
||||
"name": "type"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmptyCosmosLatentVideo": {
|
||||
"display_name": "VidéoLatenteCosmosVide",
|
||||
"inputs": {
|
||||
"batch_size": {
|
||||
"name": "taille_du_lot"
|
||||
},
|
||||
"height": {
|
||||
"name": "hauteur"
|
||||
},
|
||||
"length": {
|
||||
"name": "longueur"
|
||||
},
|
||||
"width": {
|
||||
"name": "largeur"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmptyHunyuanLatentVideo": {
|
||||
"display_name": "EmptyHunyuanLatentVideo",
|
||||
"inputs": {
|
||||
@@ -2358,6 +2431,9 @@
|
||||
"camera_type": {
|
||||
"name": "type_de_caméra"
|
||||
},
|
||||
"fov": {
|
||||
"name": "fov"
|
||||
},
|
||||
"height": {
|
||||
"name": "hauteur"
|
||||
},
|
||||
@@ -2410,6 +2486,9 @@
|
||||
"camera_type": {
|
||||
"name": "type_de_caméra"
|
||||
},
|
||||
"fov": {
|
||||
"name": "fov"
|
||||
},
|
||||
"height": {
|
||||
"name": "hauteur"
|
||||
},
|
||||
@@ -4232,6 +4311,9 @@
|
||||
"camera_type": {
|
||||
"name": "type_de_camera"
|
||||
},
|
||||
"fov": {
|
||||
"name": "fov"
|
||||
},
|
||||
"light_intensity": {
|
||||
"name": "intensité_de_lumière"
|
||||
},
|
||||
@@ -4786,6 +4868,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SetFirstSigma": {
|
||||
"display_name": "DéfinirPremierSigma",
|
||||
"inputs": {
|
||||
"sigma": {
|
||||
"name": "sigma"
|
||||
},
|
||||
"sigmas": {
|
||||
"name": "sigmas"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SetHookKeyframes": {
|
||||
"display_name": "Définir les Images Clés de Crochet",
|
||||
"inputs": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -74,6 +74,9 @@
|
||||
"Comfy_ClearWorkflow": {
|
||||
"label": "ワークフローをクリア"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "現在のワークフローを複製"
|
||||
},
|
||||
"Comfy_ExportWorkflow": {
|
||||
"label": "ワークフローをエクスポート"
|
||||
},
|
||||
@@ -149,6 +152,9 @@
|
||||
"Comfy_Undo": {
|
||||
"label": "元に戻す"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "現在のワークフローを閉じる"
|
||||
},
|
||||
"Workspace_NextOpenedWorkflow": {
|
||||
"label": "次の開いたワークフロー"
|
||||
},
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"enableAll": "すべて有効にする",
|
||||
"error": "エラー",
|
||||
"experimental": "ベータ",
|
||||
"export": "エクスポート",
|
||||
"extensionName": "拡張機能名",
|
||||
"findIssues": "問題を見つける",
|
||||
"firstTimeUIMessage": "新しいUIを初めて使用しています。「メニュー > 新しいメニューを使用 > 無効」を選択して古いUIに戻してください。",
|
||||
@@ -106,6 +107,7 @@
|
||||
"noTasksFound": "タスクが見つかりません",
|
||||
"noTasksFoundMessage": "キューにタスクがありません。",
|
||||
"noWorkflowsFound": "ワークフローが見つかりません。",
|
||||
"ok": "OK",
|
||||
"openNewIssue": "新しい問題を開く",
|
||||
"overwrite": "上書き",
|
||||
"reconnected": "再接続されました",
|
||||
@@ -128,10 +130,12 @@
|
||||
"searchWorkflows": "ワークフローを検索",
|
||||
"settings": "設定",
|
||||
"showReport": "レポートを表示",
|
||||
"success": "成功",
|
||||
"systemInfo": "システム情報",
|
||||
"terminal": "ターミナル",
|
||||
"upload": "アップロード",
|
||||
"videoFailedToLoad": "ビデオの読み込みに失敗しました"
|
||||
"videoFailedToLoad": "ビデオの読み込みに失敗しました",
|
||||
"workflow": "ワークフロー"
|
||||
},
|
||||
"graphCanvasMenu": {
|
||||
"fitView": "ビューに合わせる",
|
||||
@@ -184,6 +188,7 @@
|
||||
"selectGpu": "GPUを選択",
|
||||
"selectGpuDescription": "所有しているGPUのタイプを選択してください"
|
||||
},
|
||||
"helpImprove": "ComfyUIの改善にご協力ください",
|
||||
"installLocation": "インストール先",
|
||||
"installLocationDescription": "ComfyUIのユーザーデータを保存するディレクトリを選択してください。Python環境が選択した場所にインストールされます。選択したディスクに約15GBの空き容量が必要です。",
|
||||
"installLocationTooltip": "ComfyUIのユーザーデータディレクトリ。保存内容:\n- Python環境\n- モデル\n- カスタムノード\n",
|
||||
@@ -195,35 +200,57 @@
|
||||
"title": "マニュアル設定",
|
||||
"virtualEnvironmentPath": "仮想環境のパス"
|
||||
},
|
||||
"metricsDisabled": "メトリクス無効",
|
||||
"metricsEnabled": "メトリクス有効",
|
||||
"migrateFromExistingInstallation": "既存のインストールから移行",
|
||||
"migration": "移行",
|
||||
"migrationOptional": "移行は任意です。既存のインストールがない場合、このステップをスキップできます。",
|
||||
"migrationSourcePathDescription": "既存のComfyUIインストールがある場合、既存のユーザーファイルやモデルを新しいインストールにコピー/リンクできます。",
|
||||
"migrationSourcePathDescription": "既存のComfyUIインストールがある場合、既存のユーザーファイルとモデルを新しいインストールにコピー/リンクすることができます。既存のComfyUIインストールは影響を受けません。",
|
||||
"moreInfo": "詳細は、私たちの",
|
||||
"parentMissing": "パスが存在しません - 最初に含まれるディレクトリを作成してください",
|
||||
"pathExists": "ディレクトリはすでに存在します - すべてのデータをバックアップしたことを確認してください",
|
||||
"pathValidationFailed": "パスの検証に失敗しました",
|
||||
"privacyPolicy": "プライバシーポリシー",
|
||||
"selectItemsToMigrate": "移行する項目を選択",
|
||||
"settings": {
|
||||
"allowMetrics": "クラッシュレポート",
|
||||
"allowMetricsDescription": "ComfyUIの改善に協力してください。匿名のクラッシュレポートを送信します。個人情報やワークフロー内容は収集されません。この設定はいつでも無効にできます。",
|
||||
"allowMetrics": "使用状況のメトリクス",
|
||||
"allowMetricsDescription": "匿名の使用状況メトリクスを送信してComfyUIを改善します。個人情報やワークフローの内容は収集されません。",
|
||||
"autoUpdate": "自動更新",
|
||||
"autoUpdateDescription": "更新が利用可能になると、自動的にダウンロードおよびインストールを行います。インストール前に通知が表示されます。",
|
||||
"dataCollectionDialog": {
|
||||
"customNodeConfigurations": "カスタムノード設定",
|
||||
"errorReports": "エラーメッセージとスタックトレース",
|
||||
"fileSystemInformation": "ファイルシステム情報",
|
||||
"personalInformation": "個人情報",
|
||||
"systemInfo": "ハードウェア、OSの種類、アプリのバージョン",
|
||||
"collect": {
|
||||
"errorReports": "エラーメッセージとスタックトレース",
|
||||
"systemInfo": "ハードウェア、OSタイプ、アプリバージョン",
|
||||
"userJourneyEvents": "ユーザージャーニーイベント"
|
||||
},
|
||||
"doNotCollect": {
|
||||
"customNodeConfigurations": "カスタムノードの設定",
|
||||
"fileSystemInformation": "ファイルシステム情報",
|
||||
"personalInformation": "個人情報",
|
||||
"workflowContents": "ワークフローの内容"
|
||||
},
|
||||
"title": "データ収集について",
|
||||
"viewFullPolicy": "完全なポリシーを見る",
|
||||
"whatWeCollect": "収集内容:",
|
||||
"whatWeDoNotCollect": "収集しない内容:",
|
||||
"workflowContent": "ワークフロー内容",
|
||||
"workflowContents": "ワークフロー内容"
|
||||
"whatWeDoNotCollect": "収集しない内容:"
|
||||
},
|
||||
"errorUpdatingConsent": "同意の更新エラー",
|
||||
"errorUpdatingConsentDetail": "メトリクスの同意設定の更新に失敗しました",
|
||||
"learnMoreAboutData": "データ収集の詳細を見る"
|
||||
},
|
||||
"systemLocations": "システムの場所",
|
||||
"unhandledError": "未知のエラー"
|
||||
"unhandledError": "未知のエラー",
|
||||
"updateConsent": "以前はクラッシュの報告に同意していました。現在、バグの特定とアプリの改善を助けるためにイベントベースのメトリクスを追跡しています。個人を特定できる情報は収集されません。"
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "フォローアップのために私に連絡する",
|
||||
"helpFix": "これを修正するのを助ける",
|
||||
"notifyResolve": "解決したときに通知する",
|
||||
"provideAdditionalDetails": "追加の詳細を提供する(オプション)",
|
||||
"provideEmail": "あなたのメールアドレスを教えてください(オプション)",
|
||||
"stackTrace": "スタックトレース",
|
||||
"submitErrorReport": "エラーレポートを提出する(オプション)",
|
||||
"systemStats": "システム統計"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "自動キュー",
|
||||
@@ -257,12 +284,14 @@
|
||||
"Clear Pending Tasks": "保留中のタスクをクリア",
|
||||
"Clear Workflow": "ワークフローをクリア",
|
||||
"Clipspace": "クリップスペース",
|
||||
"Close Current Workflow": "現在のワークフローを閉じる",
|
||||
"Collapse/Expand Selected Nodes": "選択したノードの折りたたみ/展開",
|
||||
"Comfy-Org Discord": "Comfy-Org Discord",
|
||||
"ComfyUI Docs": "ComfyUIのドキュメント",
|
||||
"ComfyUI Issues": "ComfyUIの問題",
|
||||
"Convert selected nodes to group node": "選択したノードをグループノードに変換",
|
||||
"Desktop User Guide": "デスクトップユーザーガイド",
|
||||
"Duplicate Current Workflow": "現在のワークフローを複製",
|
||||
"Edit": "編集",
|
||||
"Export": "エクスポート",
|
||||
"Export (API)": "エクスポート (API)",
|
||||
@@ -620,11 +649,13 @@
|
||||
"workflows": "ワークフロー"
|
||||
},
|
||||
"tabMenu": {
|
||||
"addToBookmarks": "ブックマークに追加",
|
||||
"closeOtherTabs": "他のタブを閉じる",
|
||||
"closeTab": "タブを閉じる",
|
||||
"closeTabsToLeft": "左のタブを閉じる",
|
||||
"closeTabsToRight": "右のタブを閉じる",
|
||||
"duplicateTab": "タブを複製"
|
||||
"duplicateTab": "タブを複製",
|
||||
"removeFromBookmarks": "ブックマークから削除"
|
||||
},
|
||||
"templateWorkflows": {
|
||||
"template": {
|
||||
|
||||
@@ -113,12 +113,15 @@
|
||||
}
|
||||
},
|
||||
"CLIPLoader": {
|
||||
"description": "[レシピ]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 / clip-g / clip-l\nstable_audio: t5\nmochi: t5",
|
||||
"description": "[レシピ]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 / clip-g / clip-l\nstable_audio: t5\nmochi: t5\ncosmos: old t5 xxl",
|
||||
"display_name": "CLIPを読み込む",
|
||||
"inputs": {
|
||||
"clip_name": {
|
||||
"name": "clip名"
|
||||
},
|
||||
"device": {
|
||||
"name": "デバイス"
|
||||
},
|
||||
"type": {
|
||||
"name": "タイプ"
|
||||
}
|
||||
@@ -859,6 +862,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CosmosImageToVideoLatent": {
|
||||
"display_name": "CosmosImageToVideoLatent",
|
||||
"inputs": {
|
||||
"batch_size": {
|
||||
"name": "バッチサイズ"
|
||||
},
|
||||
"end_image": {
|
||||
"name": "終了画像"
|
||||
},
|
||||
"height": {
|
||||
"name": "高さ"
|
||||
},
|
||||
"length": {
|
||||
"name": "長さ"
|
||||
},
|
||||
"start_image": {
|
||||
"name": "開始画像"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
},
|
||||
"width": {
|
||||
"name": "幅"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateHookKeyframe": {
|
||||
"display_name": "フックキーフレームを作成",
|
||||
"inputs": {
|
||||
@@ -1100,6 +1129,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"DevToolsNodeWithSeedInput": {
|
||||
"description": "シード入力付きのノード",
|
||||
"display_name": "シード入力付きノード",
|
||||
"inputs": {
|
||||
"seed": {
|
||||
"name": "シード"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DevToolsNodeWithStringInput": {
|
||||
"description": "文字列入力を持つノード",
|
||||
"display_name": "文字列入力ノード",
|
||||
@@ -1124,6 +1162,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"DevToolsObjectPatchNode": {
|
||||
"description": "オブジェクトパッチを適用するノード",
|
||||
"display_name": "オブジェクトパッチノード",
|
||||
"inputs": {
|
||||
"dummy_float": {
|
||||
"name": "ダミーフロート"
|
||||
},
|
||||
"model": {
|
||||
"name": "モデル"
|
||||
},
|
||||
"target_module": {
|
||||
"name": "ターゲットモジュール"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DevToolsSimpleSlider": {
|
||||
"display_name": "シンプルスライダー",
|
||||
"inputs": {
|
||||
@@ -1195,11 +1248,31 @@
|
||||
"clip_name2": {
|
||||
"name": "clip_name2"
|
||||
},
|
||||
"device": {
|
||||
"name": "デバイス"
|
||||
},
|
||||
"type": {
|
||||
"name": "タイプ"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmptyCosmosLatentVideo": {
|
||||
"display_name": "EmptyCosmosLatentVideo",
|
||||
"inputs": {
|
||||
"batch_size": {
|
||||
"name": "バッチサイズ"
|
||||
},
|
||||
"height": {
|
||||
"name": "高さ"
|
||||
},
|
||||
"length": {
|
||||
"name": "長さ"
|
||||
},
|
||||
"width": {
|
||||
"name": "幅"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmptyHunyuanLatentVideo": {
|
||||
"display_name": "EmptyHunyuanLatentVideo",
|
||||
"inputs": {
|
||||
@@ -2358,6 +2431,9 @@
|
||||
"camera_type": {
|
||||
"name": "カメラタイプ"
|
||||
},
|
||||
"fov": {
|
||||
"name": "fov"
|
||||
},
|
||||
"height": {
|
||||
"name": "高さ"
|
||||
},
|
||||
@@ -2410,6 +2486,9 @@
|
||||
"camera_type": {
|
||||
"name": "カメラタイプ"
|
||||
},
|
||||
"fov": {
|
||||
"name": "fov"
|
||||
},
|
||||
"height": {
|
||||
"name": "高さ"
|
||||
},
|
||||
@@ -4232,6 +4311,9 @@
|
||||
"camera_type": {
|
||||
"name": "カメラタイプ"
|
||||
},
|
||||
"fov": {
|
||||
"name": "fov"
|
||||
},
|
||||
"light_intensity": {
|
||||
"name": "光の強度"
|
||||
},
|
||||
@@ -4786,6 +4868,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SetFirstSigma": {
|
||||
"display_name": "SetFirstSigma",
|
||||
"inputs": {
|
||||
"sigma": {
|
||||
"name": "シグマ"
|
||||
},
|
||||
"sigmas": {
|
||||
"name": "シグマ"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SetHookKeyframes": {
|
||||
"display_name": "フックキーフレームを設定",
|
||||
"inputs": {
|
||||
|
||||