Compare commits

...

118 Commits

Author SHA1 Message Date
christian-byrne
11c46ea62d Refactor Comfy version retrieval and update changelog version description 2025-01-18 13:04:22 -07:00
christian-byrne
86789ceb9d Show changelog on new version 2025-01-18 12:25:04 -07:00
Chenlei Hu
1a4e77a3ab [Desktop] Ctrl+w to close workflow tab (#2282) 2025-01-17 20:02:54 -05:00
Chenlei Hu
de570712df 1.7.14 (#2281) 2025-01-17 18:04:43 -05:00
Chenlei Hu
44612e8f97 [Desktop] Add privacy policy link to install view about dialog (#2280)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-17 18:04:08 -05:00
bymyself
3df911c1bf Add consent prompt view (#2268)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-17 17:39:50 -05:00
Chenlei Hu
af26b9ad6d [Desktop] Report completed generation status (#2279) 2025-01-17 17:36:46 -05:00
Chenlei Hu
d503873980 Move queueStore update from QueueSidebarTab to GraphView (#2278) 2025-01-17 17:14:22 -05:00
bymyself
842a9f74fc [BrowserTest] Fix flaky gallery test (#2150) 2025-01-17 17:11:49 -05:00
Chenlei Hu
29551a36b3 Add .cursorrules (#2277) 2025-01-17 16:35:25 -05:00
filtered
d6e5c8950c [Desktop] Loading screen (#2274)
Co-authored-by: huchenlei <huchenlei@proton.me>
2025-01-17 11:12:03 -05:00
Chenlei Hu
ad1c1ce9c2 [chore] Update electron-types to 0.4.9 (#2276) 2025-01-17 10:59:41 -05:00
Benjamin Lu
cb9d2c6bae Caching brush settings in mask editor (#2271)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-01-17 10:38:23 -05:00
Chenlei Hu
7fd41eeaba 1.7.13 (#2270) 2025-01-16 11:51:58 -05:00
filtered
79fee6ac72 Fix collapsed node textarea causes UI inconsistency (#2267) 2025-01-16 11:50:53 -05:00
Benjamin Lu
edd58cd153 Hotfix scoped --sidebar-width uasges in maskeditor.ts (#2269)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-01-16 11:36:11 -05:00
Chenlei Hu
e153508955 1.7.12 (#2265) 2025-01-15 20:19:24 -05:00
Chenlei Hu
237fca0bf1 [CodeHealth] Use scoped CSS for SideToolbar (#2264) 2025-01-15 20:16:39 -05:00
Chenlei Hu
65542b885a [Style] Fix root CSS selector (#2263) 2025-01-15 19:44:46 -05:00
Chenlei Hu
f739f704af [CodeHealth] Use scoped CSS in views (#2262) 2025-01-15 19:35:01 -05:00
Chenlei Hu
37abdbe35d [Desktop] Add install screen stepper change metrics (#2261) 2025-01-15 19:27:05 -05:00
Chenlei Hu
ff445f5c95 Apply min col on logs terminal for colab (#2260) 2025-01-15 17:16:33 -05:00
Chenlei Hu
84b652a281 [CodeHealth] Convert useAutoSize to kwargs (#2259) 2025-01-15 17:06:45 -05:00
Chenlei Hu
184291d21b [Settings] Enable Comfy.Window.UnloadConfirmation by default (#2258) 2025-01-15 16:37:11 -05:00
Chenlei Hu
d7fb25a36a Don't prompt unsaved when there is no unsaved workflow on window close (#2257) 2025-01-15 16:34:05 -05:00
Comfy Org PR Bot
c039a60fcc Update locales for node definitions (#2256)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
Co-authored-by: huchenlei <huchenlei@proton.me>
2025-01-15 16:23:45 -05:00
Chenlei Hu
3b6108c26e Add work dir to i18n-node-defs.yaml (#2255) 2025-01-15 16:19:13 -05:00
Chenlei Hu
49bb247526 1.7.11 (#2250) 2025-01-14 23:34:58 -05:00
bymyself
dd005f5fa5 Allow parent component to pass tags to issue report panel (#2247) 2025-01-14 23:34:11 -05:00
Chenlei Hu
bf90b458d3 [Desktop] Add clarification of migration from existing install (#2249)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-14 23:33:53 -05:00
bymyself
7e78c5b1dc Fix type error in BaseViewTemplate (#2245) 2025-01-14 20:19:35 -05:00
Gremlation
c13190cd07 Fix execution_interrupted (#2244) 2025-01-14 11:46:42 -05:00
filtered
00f031e382 [Refactor] Remove old workarounds (#2242) 2025-01-14 10:52:34 -05:00
filtered
04153caaf5 Fix prettier output in CI does not match IDE (#2243) 2025-01-14 10:51:32 -05:00
Chenlei Hu
210bfdeb7d 1.7.10 (#2241) 2025-01-13 20:25:36 -05:00
bymyself
ce0726d85e Restore all open workflows on load (#2238) 2025-01-13 20:24:40 -05:00
bymyself
dd69f9dc30 [Style] Update workflow template cards style (#2239) 2025-01-13 20:21:03 -05:00
Chenlei Hu
3f261f0e53 [Desktop] Add user journey events to the metrics collection list (UI) (#2237)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-12 17:16:16 -05:00
Chenlei Hu
3b2cc23f65 [Desktop] Mark window style setting as experimental (#2236) 2025-01-12 16:14:51 -05:00
bymyself
c50a86b258 [CI] Fix vite config condition (#2235) 2025-01-12 16:09:46 -05:00
Chenlei Hu
1a8c2bba42 1.7.9 (#2234) 2025-01-12 13:43:05 -05:00
Benjamin Lu
fc09951b3e [Style] Visual improvements to WorkflowTabs (#2232)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-01-12 13:41:55 -05:00
Terry Jia
76d5f39607 [3d] use threejs native viewHelper (#2230) 2025-01-12 13:23:23 -05:00
bymyself
9d3bc0f173 Add optional report feature to error dialog (#2229)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-12 13:23:02 -05:00
bymyself
d9b350e159 Add bookmark option in workflow tab context menu (#2231)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-12 13:22:22 -05:00
Chenlei Hu
44610674ee [Desktop] Fix server start view layout (#2226) 2025-01-10 22:14:01 -05:00
Chenlei Hu
9bfce5b8d0 Disable i18n on PRs from forked repo (#2225) 2025-01-10 19:23:20 -05:00
Chenlei Hu
8986fa356a 1.7.8 (#2223) 2025-01-10 19:06:03 -05:00
Chenlei Hu
0c4fd4af1c [Style] Set topbar height to 2.5rem (40px) (#2224) 2025-01-10 19:05:44 -05:00
Chenlei Hu
30cd46ce1f [Desktop] Allow dragging window on empty titlebar (#2222) 2025-01-10 13:41:26 -05:00
filtered
3122c33310 Allow Ctrl + C to send interrupt in terminal (#2221) 2025-01-10 13:30:11 -05:00
Chenlei Hu
91d8d04dc6 [Style] Reduce opacity on workflow tabs scrollbar (#2220) 2025-01-10 10:59:58 -05:00
Chenlei Hu
8f5aa1ff08 [Desktop] Native window in graph view (#2216) 2025-01-09 23:00:28 -05:00
Chenlei Hu
e076783b89 [Desktop] Native window virtual menu bar (#2215) 2025-01-09 20:21:03 -05:00
Chenlei Hu
04c23001fc [Desktop] Set window action buttons style (#2214) 2025-01-09 19:35:22 -05:00
Chenlei Hu
cb265fb0bf 1.7.7 (#2213) 2025-01-09 19:03:43 -05:00
Chenlei Hu
e9211fe377 [Desktop] Add window style setting (#2212)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-09 17:33:57 -05:00
Chenlei Hu
ffc7febeac [BrowserTest] Disable flaky test (#2211) 2025-01-09 14:46:09 -05:00
Chenlei Hu
b15e626607 [Style] Replace px with rem in sidebar width (#2210) 2025-01-09 14:25:34 -05:00
Chenlei Hu
906b5e35a3 [Style] Set fixed top menu bar height (#2209) 2025-01-09 14:14:14 -05:00
Chenlei Hu
e8cd9c7642 [BrowserTest] Replace ComfyPage.reload with ComfyPage.setup (#2208) 2025-01-09 12:54:05 -05:00
Chenlei Hu
93e184e379 Explicitly add zod-to-json-schema as dev dependency (#2207) 2025-01-09 11:41:40 -05:00
Chenlei Hu
1d02cd3c47 [Docs] Extract comfy workflow zod schema as json schema (#2206) 2025-01-09 11:30:47 -05:00
Chenlei Hu
b86e3f71cb 1.7.6 (#2202) 2025-01-08 11:24:59 -05:00
bymyself
1ece2462bd Use more conventional import/export icons in color palette settings (#2199)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-08 10:58:21 -05:00
bymyself
73ecacfa2d Fix searchbox dismissed when closing filter panel (#2196) 2025-01-08 10:57:55 -05:00
bymyself
67e6df7c72 Fix search result category text on light theme (#2198) 2025-01-08 10:57:10 -05:00
Jarvis
7e8510028d fix: Make sure use the correct graph parameter (#2200) 2025-01-08 10:56:27 -05:00
bymyself
dd4dd8b68a [Doc] Add anchors to sections in README (#2197) 2025-01-08 10:55:59 -05:00
bymyself
7111022617 Fix node text highlight color on light theme (#2195) 2025-01-08 09:16:57 -05:00
Chenlei Hu
daee073045 [Style] Fix second row workflow tabs overflow (#2194) 2025-01-07 16:28:49 -05:00
Chenlei Hu
a1a834a76d Support 2nd row location for workflow tabs (#2193)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-07 16:05:37 -05:00
Chenlei Hu
c437d32691 Truncate sidebar title first to avoid tool buttons wrap (#2192) 2025-01-07 15:14:09 -05:00
Chenlei Hu
0130d41be5 Try fix workflow tabs vertical scrollbar (#2191) 2025-01-07 13:39:14 -05:00
Chenlei Hu
527561d148 1.7.5 (#2189) 2025-01-07 10:47:34 -05:00
Chenlei Hu
1c4481c342 Horizontal scroll workflow tabs on overflow (#2188) 2025-01-07 10:45:58 -05:00
Chenlei Hu
07000a23d4 Wrap each init task with error handling (#2187) 2025-01-07 10:40:32 -05:00
Chenlei Hu
ea6c9e7ca5 Replace unset unknown keybinding error with warn (#2186) 2025-01-07 10:33:23 -05:00
Chenlei Hu
90698fced6 [BugFix] Migrate deprecated Comfy.Keybinding.UnsetBindings values (#2185) 2025-01-07 10:23:25 -05:00
iola1999
9716aea10d fix: Fix infinite recursion in mask upload retry logic (#2181) 2025-01-07 16:19:13 +01:00
Chenlei Hu
077ded2cce [Refactor] Simplify keybindingStore with _.groupBy (#2180) 2025-01-06 19:46:20 -05:00
Chenlei Hu
ffb20b8789 1.7.4 (#2179) 2025-01-06 19:24:40 -05:00
Chenlei Hu
549a2fdc92 Remove main_repo_release.py (#2147) 2025-01-06 17:01:41 -05:00
bymyself
e123295423 Fix saved keybinding persistence (#2176) 2025-01-06 15:54:35 -05:00
Chenlei Hu
613b44610a 1.7.3 (#2175) 2025-01-06 11:04:54 -05:00
Chenlei Hu
e82d795ff9 Trigger node def locale update manually (#2170) 2025-01-06 10:54:59 -05:00
bymyself
ba31f8fa68 Add Close Workflow to commands (#2171)
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Chenlei Hu <huchenlei@proton.me>
2025-01-06 10:50:44 -05:00
Alexander Piskun
f2eb4e1519 (fix): added missing slash to "loadWorkflow" templates endpoint (#2174) 2025-01-06 10:49:07 -05:00
Chenlei Hu
975c2248c5 Rename Keybinding.targetSelector to targetElementId (#2169)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-05 16:03:38 -05:00
bymyself
477f4b275d Fix node bookmark color customization (#2168) 2025-01-05 13:05:09 -05:00
bymyself
179b8c22a9 [BrowserTest] Refactor test fixtures (#2165) 2025-01-05 13:03:07 -05:00
bymyself
6525389273 Update Settings schema (#2167) 2025-01-05 12:56:29 -05:00
Chenlei Hu
59fc5ac77e Update litegraph 0.8.60 (#2164) 2025-01-04 20:46:38 -05:00
bymyself
ed844e04b8 Fix text wrapping in dropdown menu (#2162) 2025-01-04 20:30:58 -05:00
bymyself
972af1977d Improve menu accessibility (#2163) 2025-01-04 20:30:27 -05:00
Chenlei Hu
5ac0b7b181 1.7.2 (#2161) 2025-01-04 19:42:15 -05:00
Chenlei Hu
31ea39e44c Prevent reference sharing on settingStore content (#2160) 2025-01-04 19:31:32 -05:00
Chenlei Hu
e65653c107 Add support for custom light color palette (#2156)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-04 18:53:47 -05:00
Chenlei Hu
e46706777c [BrowserTest] Disable flaky queue sidebar tests (#2158) 2025-01-04 16:33:36 -05:00
Chenlei Hu
1a817a48cb 1.7.1 (#2152) 2025-01-03 20:19:11 -05:00
Chenlei Hu
871967349f 1.7.0 (#2151)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-03 20:14:51 -05:00
filtered
11258f4a95 Revert markdown note node, reland as new node (#2148) 2025-01-03 16:05:23 -05:00
bymyself
c3e05c2a10 Add alt text to images in gallery and queue (#2136) 2025-01-03 13:49:06 -05:00
Chenlei Hu
ea1e776dcc Update litegraph 0.8.59 (#2140) 2025-01-03 11:15:16 -05:00
Dr.Lt.Data
f0c273f845 elaborate korean translation (#2137) 2025-01-03 11:14:56 -05:00
Chenlei Hu
ea489851ed [BugFix] Fix open workflow after insert (#2138) 2025-01-03 11:09:28 -05:00
Chenlei Hu
5717c33a0b [BrowserTest] Add test on paste with link (#2130)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-02 20:38:36 -05:00
Chenlei Hu
c31919c418 Remove invertMenuScrolling extension (#2131)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-02 20:19:00 -05:00
bymyself
2e7de4701e Reduce debounce delay in settings search (#2126) 2025-01-02 19:51:31 -05:00
Chenlei Hu
f53723da0f 1.6.15 (#2129) 2025-01-02 17:54:31 -05:00
bymyself
5c7cbe968e Fix gallery changing image every time a task finishes (#2125) 2025-01-02 17:51:28 -05:00
Chenlei Hu
cb607ee070 Update litegraph 0.8.58 (#2128) 2025-01-02 17:47:32 -05:00
Terry Jia
f4b5677901 [3d] support for fov and mask (#2116) 2025-01-02 15:45:37 -05:00
Chenlei Hu
9c1eacf0af Fix duplication of persisted workflow (#2124)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-02 12:16:39 -05:00
Chenlei Hu
bb988edf9f 1.6.14 (#2120) 2025-01-01 19:29:47 -05:00
Chenlei Hu
39e9f421f4 Fix double trigger of loadColorPalette effect (#2118)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-01 19:27:22 -05:00
Chenlei Hu
3d2b9a8d9d Call api.storeSetting only when setting value changes (#2119) 2025-01-01 17:34:58 -05:00
pythongosssss
c77a5cab5b Add support for nested dynamic prompts (#2117) 2025-01-01 15:29:17 -05:00
148 changed files with 4548 additions and 1210 deletions

43
.cusorrules Normal file
View 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
View 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

View File

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

View File

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

5
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,8 +1,9 @@
import { expect } from '@playwright/test'
import type { Palette } from '../src/types/colorPaletteTypes'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
const customColorPalettes = {
const customColorPalettes: Record<string, Palette> = {
obsidian: {
version: 102,
id: 'obsidian',
@@ -128,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(

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

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

View File

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

View File

@@ -27,6 +27,11 @@ dotenv.config()
type WorkspaceStore = ReturnType<typeof useWorkspaceStore>
class ComfyMenu {
private _nodeLibraryTab: NodeLibrarySidebarTab | null = null
private _workflowsTab: WorkflowsSidebarTab | null = null
private _queueTab: QueueSidebarTab | null = null
private _topbar: Topbar | null = null
public readonly sideToolbar: Locator
public readonly themeToggleButton: Locator
public readonly saveButton: Locator
@@ -40,19 +45,23 @@ class ComfyMenu {
}
get nodeLibraryTab() {
return new NodeLibrarySidebarTab(this.page)
this._nodeLibraryTab ??= new NodeLibrarySidebarTab(this.page)
return this._nodeLibraryTab
}
get workflowsTab() {
return new WorkflowsSidebarTab(this.page)
this._workflowsTab ??= new WorkflowsSidebarTab(this.page)
return this._workflowsTab
}
get queueTab() {
return new QueueSidebarTab(this.page)
this._queueTab ??= new QueueSidebarTab(this.page)
return this._queueTab
}
get topbar() {
return new Topbar(this.page)
this._topbar ??= new Topbar(this.page)
return this._topbar
}
async toggleTheme() {
@@ -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(
(

View File

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

View File

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

View File

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

View File

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

View File

@@ -469,6 +469,43 @@ test.describe('Canvas Interaction', () => {
expect(await getCursorStyle()).toBe('default')
})
// https://github.com/Comfy-Org/litegraph.js/pull/424
test('Properly resets dragging state after pan mode sequence', async ({
comfyPage
}) => {
const getCursorStyle = async () => {
return await comfyPage.page.evaluate(() => {
return (
document.getElementById('graph-canvas')!.style.cursor || 'default'
)
})
}
// Initial state check
await comfyPage.page.mouse.move(10, 10)
expect(await getCursorStyle()).toBe('default')
// Click and hold
await comfyPage.page.mouse.down()
expect(await getCursorStyle()).toBe('grabbing')
// Press space while holding click
await comfyPage.page.keyboard.down('Space')
expect(await getCursorStyle()).toBe('grabbing')
// Release click while space is still down
await comfyPage.page.mouse.up()
expect(await getCursorStyle()).toBe('grab')
// Release space
await comfyPage.page.keyboard.up('Space')
expect(await getCursorStyle()).toBe('default')
// Move mouse - cursor should remain default
await comfyPage.page.mouse.move(20, 20)
expect(await getCursorStyle()).toBe('default')
})
test('Can pan when dragging a link', async ({ comfyPage }) => {
const posSlot1 = comfyPage.clipTextEncodeNode1InputSlot
await comfyPage.page.mouse.move(posSlot1.x, posSlot1.y)
@@ -556,7 +593,7 @@ test.describe('Load workflow', () => {
}) => {
await comfyPage.loadWorkflow('single_ksampler')
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
await comfyPage.reload({ clearStorage: false })
await comfyPage.setup({ clearStorage: false })
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
})
@@ -573,11 +610,72 @@ test.describe('Load workflow', () => {
await expect(comfyPage.canvas).toHaveScreenshot(
'single_ksampler_modified.png'
)
await comfyPage.reload({ clearStorage: false })
await comfyPage.setup({ clearStorage: false })
await expect(comfyPage.canvas).toHaveScreenshot(
'single_ksampler_modified.png'
)
})
test.describe('Restore all open workflows on reload', () => {
let workflowA: string
let workflowB: string
const generateUniqueFilename = (extension = '') =>
`${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}${extension}`
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
workflowA = generateUniqueFilename()
await comfyPage.menu.topbar.saveWorkflow(workflowA)
workflowB = generateUniqueFilename()
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
await comfyPage.menu.topbar.saveWorkflow(workflowB)
// Wait for localStorage to persist the workflow paths before reloading
await comfyPage.page.waitForFunction(
() => !!window.localStorage.getItem('Comfy.OpenWorkflowsPaths')
)
await comfyPage.setup({ clearStorage: false })
})
test('Restores topbar workflow tabs after reload', async ({
comfyPage
}) => {
await comfyPage.setSetting(
'Comfy.Workflow.WorkflowTabsPosition',
'Topbar'
)
const tabs = await comfyPage.menu.topbar.getTabNames()
const activeWorkflowName = await comfyPage.menu.topbar.getActiveTabName()
expect(tabs).toEqual(expect.arrayContaining([workflowA, workflowB]))
expect(tabs.indexOf(workflowA)).toBeLessThan(tabs.indexOf(workflowB))
expect(activeWorkflowName).toEqual(workflowB)
})
test('Restores sidebar workflows after reload', async ({ comfyPage }) => {
await comfyPage.setSetting(
'Comfy.Workflow.WorkflowTabsPosition',
'Sidebar'
)
await comfyPage.menu.workflowsTab.open()
const openWorkflows =
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
const activeWorkflowName =
await comfyPage.menu.workflowsTab.getActiveWorkflowName()
const workflowPathA = `${workflowA}.json`
const workflowPathB = `${workflowB}.json`
expect(openWorkflows).toEqual(
expect.arrayContaining([workflowPathA, workflowPathB])
)
expect(openWorkflows.indexOf(workflowPathA)).toBeLessThan(
openWorkflows.indexOf(workflowPathB)
)
expect(activeWorkflowName).toEqual(workflowPathB)
})
})
})
test.describe('Load duplicate workflow', () => {

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

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

2
global.d.ts vendored
View File

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

130
package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,12 +5,20 @@
:message="props.error.exception_message"
/>
<div class="comfy-error-report">
<Button
v-show="!reportOpen"
:label="$t('g.showReport')"
@click="showReport"
text
/>
<div class="flex gap-2 justify-center">
<Button
v-show="!reportOpen"
text
:label="$t('g.showReport')"
@click="showReport"
/>
<Button
v-show="!sendReportOpen"
text
:label="$t('issueReport.helpFix')"
@click="showSendReport"
/>
</div>
<template v-if="reportOpen">
<Divider />
<ScrollPanel style="width: 100%; height: 400px; max-width: 80vw">
@@ -18,9 +26,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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,6 +13,9 @@
<BottomPanel />
</template>
<template #graph-canvas-panel>
<SecondRowWorkflowTabs
v-if="workflowTabsPosition === 'Topbar (2nd-row)'"
/>
<GraphCanvasMenu v-if="canvasMenuEnabled" />
</template>
</LiteGraphCanvasSplitterOverlay>
@@ -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'),

View File

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

View File

@@ -59,10 +59,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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,7 @@
:key="selectedTab.moduleName"
>
<template #item="slotProps">
<div @click="loadWorkflow(slotProps.data)">
<div @click="loadWorkflow(slotProps.data)" class="p-2">
<TemplateWorkflowCard
:moduleName="selectedTab.moduleName"
:workflowName="slotProps.data"
@@ -112,7 +112,7 @@ const loadWorkflow = async (id: string) => {
let json
if (selectedTab.value.moduleName === 'default') {
// Default templates provided by frontend are served on this separate endpoint
json = await fetch(api.fileURL(`templates/${id}.json`)).then((r) =>
json = await fetch(api.fileURL(`/templates/${id}.json`)).then((r) =>
r.json()
)
} else {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -85,7 +85,8 @@ export const CORE_SETTINGS: SettingParams[] = [
name: 'Sidebar size',
type: 'combo',
options: ['normal', 'small'],
defaultValue: () => (window.innerWidth < 1600 ? 'small' : 'normal')
// Default to small if the window is less than 1536px(2xl) wide.
defaultValue: () => (window.innerWidth < 1536 ? 'small' : 'normal')
},
{
id: 'Comfy.TextareaWidget.FontSize',
@@ -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'
}
]

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
import { IWidget } from '@comfyorg/litegraph'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
@@ -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
)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,10 @@ import {
LiteGraph
} from '@comfyorg/litegraph'
import {
DEFAULT_DARK_COLOR_PALETTE,
DEFAULT_LIGHT_COLOR_PALETTE
} from '@/constants/coreColorPalettes'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
@@ -16,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)
}
}
]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -85,6 +85,7 @@
"enableAll": "Activer tout",
"error": "Erreur",
"experimental": "BETA",
"export": "Exportation",
"extensionName": "Nom de l'extension",
"findIssues": "Trouver des problèmes",
"firstTimeUIMessage": "C'est la première fois que vous utilisez la nouvelle interface utilisateur. Choisissez \"Menu > Utiliser le nouveau menu > Désactivé\" pour restaurer l'ancienne interface utilisateur.",
@@ -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": {

View File

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

View File

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

View File

@@ -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": "次の開いたワークフロー"
},

View File

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

View File

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

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