mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-14 03:30:37 +00:00
Compare commits
51 Commits
v1.7.4
...
node-group
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1f30b96f9 | ||
|
|
c13190cd07 | ||
|
|
00f031e382 | ||
|
|
04153caaf5 | ||
|
|
210bfdeb7d | ||
|
|
ce0726d85e | ||
|
|
dd69f9dc30 | ||
|
|
3f261f0e53 | ||
|
|
3b2cc23f65 | ||
|
|
c50a86b258 | ||
|
|
1a8c2bba42 | ||
|
|
fc09951b3e | ||
|
|
76d5f39607 | ||
|
|
9d3bc0f173 | ||
|
|
d9b350e159 | ||
|
|
44610674ee | ||
|
|
9bfce5b8d0 | ||
|
|
8986fa356a | ||
|
|
0c4fd4af1c | ||
|
|
30cd46ce1f | ||
|
|
3122c33310 | ||
|
|
91d8d04dc6 | ||
|
|
8f5aa1ff08 | ||
|
|
e076783b89 | ||
|
|
04c23001fc | ||
|
|
cb265fb0bf | ||
|
|
e9211fe377 | ||
|
|
ffc7febeac | ||
|
|
b15e626607 | ||
|
|
906b5e35a3 | ||
|
|
e8cd9c7642 | ||
|
|
93e184e379 | ||
|
|
1d02cd3c47 | ||
|
|
b86e3f71cb | ||
|
|
1ece2462bd | ||
|
|
73ecacfa2d | ||
|
|
67e6df7c72 | ||
|
|
7e8510028d | ||
|
|
dd4dd8b68a | ||
|
|
7111022617 | ||
|
|
daee073045 | ||
|
|
a1a834a76d | ||
|
|
c437d32691 | ||
|
|
0130d41be5 | ||
|
|
527561d148 | ||
|
|
1c4481c342 | ||
|
|
07000a23d4 | ||
|
|
ea6c9e7ca5 | ||
|
|
90698fced6 | ||
|
|
9716aea10d | ||
|
|
077ded2cce |
2
.github/workflows/i18n.yaml
vendored
2
.github/workflows/i18n.yaml
vendored
@@ -5,6 +5,8 @@ on:
|
||||
|
||||
jobs:
|
||||
update-locales:
|
||||
# Don't run on fork PRs
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
|
||||
|
||||
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -25,6 +25,8 @@ jobs:
|
||||
id: current_version
|
||||
run: echo ::set-output name=version::$(node -p "require('./package.json').version")
|
||||
- name: Build project
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -41,4 +41,7 @@ browser_tests/*/*-win32.png
|
||||
|
||||
dist.zip
|
||||
|
||||
/temp/
|
||||
/temp/
|
||||
|
||||
# Generated JSON Schemas
|
||||
/schemas/
|
||||
|
||||
12
.prettierrc
12
.prettierrc
@@ -6,5 +6,13 @@
|
||||
"printWidth": 80,
|
||||
"importOrder": ["^@core/(.*)$", "<THIRD_PARTY_MODULES>", "^@/(.*)$", "^[./]"],
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true
|
||||
}
|
||||
"importOrderSortSpecifiers": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.{js,cjs,mjs,ts,cts,mts,tsx,vue}",
|
||||
"options": {
|
||||
"plugins": ["@trivago/prettier-plugin-sort-imports"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
52
README.md
52
README.md
@@ -58,7 +58,7 @@ Stable releases are published bi-weekly in the ComfyUI main repository.
|
||||
|
||||
### Major features
|
||||
|
||||
<details>
|
||||
<details id='feature-native-translation'>
|
||||
<summary>v1.5: Native translation (i18n)</summary>
|
||||
|
||||
ComfyUI now includes built-in translation support, replacing the need for third-party translation extensions. Select your language
|
||||
@@ -68,7 +68,7 @@ Stable releases are published bi-weekly in the ComfyUI main repository.
|
||||
More details available here: https://blog.comfy.org/p/native-localization-support-i18n
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='feature-mask-editor'>
|
||||
<summary>v1.4: New mask editor</summary>
|
||||
|
||||
https://github.com/Comfy-Org/ComfyUI_frontend/pull/1284 implements a new mask editor.
|
||||
@@ -76,7 +76,7 @@ Stable releases are published bi-weekly in the ComfyUI main repository.
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='feature-integrated-server-terminal'>
|
||||
<summary>v1.3.22: Integrated server terminal</summary>
|
||||
|
||||
Press Ctrl + ` to toggle integrated terminal.
|
||||
@@ -84,7 +84,7 @@ Press Ctrl + ` to toggle integrated terminal.
|
||||
https://github.com/user-attachments/assets/eddedc6a-07a3-4a83-9475-63b3977f6d94
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='feature-keybinding-customization'>
|
||||
<summary>v1.3.7: Keybinding customization</summary>
|
||||
|
||||
## Basic UI
|
||||
@@ -101,7 +101,7 @@ https://github.com/user-attachments/assets/eddedc6a-07a3-4a83-9475-63b3977f6d94
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='feature-node-library-sidebar'>
|
||||
<summary>v1.2.4: Node library sidebar tab</summary>
|
||||
|
||||
#### Drag & Drop
|
||||
@@ -111,13 +111,13 @@ https://github.com/user-attachments/assets/853e20b7-bc0e-49c9-bbce-a2ba7566f92f
|
||||
https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='feature-queue-sidebar'>
|
||||
<summary>v1.2.0: Queue/History sidebar tab</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/86e264fe-4d26-4f07-aa9a-83bdd2d02b8f
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='feature-node-search'>
|
||||
<summary>v1.1.0: Node search box</summary>
|
||||
|
||||
#### Fuzzy search & Node preview
|
||||
@@ -129,26 +129,26 @@ https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf
|
||||
|
||||
### QoL changes
|
||||
|
||||
<details>
|
||||
<details id='feature-nested-group'>
|
||||
<summary>v1.3.32: **Litegraph** Nested group</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/f51adeb1-028e-40af-81e4-0ac13075198a
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='feature-group-selection'>
|
||||
<summary>v1.3.24: **Litegraph** Group selection</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/e6230a94-411e-4fba-90cb-6c694200adaa
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='feature-toggle-link-visibility'>
|
||||
<summary>v1.3.6: **Litegraph** Toggle link visibility</summary>
|
||||
|
||||
[rec.webm](https://github.com/user-attachments/assets/34e460ac-fbbc-44ef-bfbb-99a84c2ae2be)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='feature-auto-widget-conversion'>
|
||||
<summary>v1.3.4: **Litegraph** Auto widget to input conversion</summary>
|
||||
|
||||
Dropping a link of correct type on node widget will automatically convert the widget to input.
|
||||
@@ -157,7 +157,7 @@ Dropping a link of correct type on node widget will automatically convert the wi
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='feature-pan-mode'>
|
||||
<summary>v1.3.4: **Litegraph** Canvas pan mode</summary>
|
||||
|
||||
The canvas becomes readonly in pan mode. Pan mode is activated by clicking the pan mode button on the canvas menu
|
||||
@@ -167,42 +167,42 @@ or by holding the space key.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='feature-shift-drag-link-creation'>
|
||||
<summary>v1.3.1: **Litegraph** Shift drag link to create a new link</summary>
|
||||
|
||||
[rec.webm](https://github.com/user-attachments/assets/7e73aaf9-79e2-4c3c-a26a-911cba3b85e4)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='feature-optional-input-donuts'>
|
||||
<summary>v1.2.62: **Litegraph** Show optional input slots as donuts</summary>
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='feature-group-title-edit'>
|
||||
<summary>v1.2.44: **Litegraph** Double click group title to edit</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/5bf0e2b6-8b3a-40a7-b44f-f0879e9ad26f
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='feature-group-selection-shortcut'>
|
||||
<summary>v1.2.39: **Litegraph** Group selected nodes with Ctrl + G</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/7805dc54-0854-4a28-8bcd-4b007fa01151
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='feature-node-title-edit'>
|
||||
<summary>v1.2.38: **Litegraph** Double click node title to edit</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/d61d5d0e-f200-4153-b293-3e3f6a212b30
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='feature-drag-multi-link'>
|
||||
<summary>v1.2.7: **Litegraph** drags multiple links with shift pressed</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/68826715-bb55-4b2a-be6e-675cfc424afe
|
||||
@@ -211,7 +211,7 @@ https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='feature-auto-connect-link'>
|
||||
<summary>v1.2.2: **Litegraph** auto connects to correct slot</summary>
|
||||
|
||||
#### Before
|
||||
@@ -221,7 +221,7 @@ https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d
|
||||
https://github.com/user-attachments/assets/b6360ac0-f0d2-447c-9daa-8a2e20c0dc1d
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='feature-hide-text-overflow'>
|
||||
<summary>v1.1.8: **Litegraph** hides text overflow on widget value</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/5696a89d-4a47-4fcc-9e8c-71e1264943f2
|
||||
@@ -298,7 +298,7 @@ app.registerExtension({
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='extension-api-bottom-panel-tabs'>
|
||||
<summary>v1.3.22: Register bottom panel tabs</summary>
|
||||
|
||||
```js
|
||||
@@ -321,7 +321,7 @@ app.registerExtension({
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='extension-api-settings'>
|
||||
<summary>v1.3.22: New settings API</summary>
|
||||
|
||||
Legacy settings API.
|
||||
@@ -367,7 +367,7 @@ app.extensionManager.setting.set('TestSetting', 'Hello, universe!')
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='extension-api-commands-keybindings'>
|
||||
<summary>v1.3.7: Register commands and keybindings</summary>
|
||||
|
||||
Extensions can call the following API to register commands and keybindings. Do
|
||||
@@ -396,7 +396,7 @@ app.extensionManager.setting.set('TestSetting', 'Hello, universe!')
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='extension-api-topbar-menu'>
|
||||
<summary>v1.3.1: Extension API to register custom topbar menu items</summary>
|
||||
|
||||
Extensions can call the following API to register custom topbar menu items.
|
||||
@@ -425,7 +425,7 @@ app.extensionManager.setting.set('TestSetting', 'Hello, universe!')
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='extension-api-toast'>
|
||||
<summary>v1.2.27: Extension API to add toast message</summary>i
|
||||
|
||||
Extensions can call the following API to add toast messages.
|
||||
@@ -443,7 +443,7 @@ Documentation of all supported options can be found here: <https://primevue.org/
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<details id='extension-api-sidebar-tab'>
|
||||
<summary>v1.2.4: Extension API to register custom sidebar tab</summary>
|
||||
|
||||
Extensions now can call the following API to register a sidebar tab.
|
||||
|
||||
@@ -150,7 +150,7 @@ test.describe('Color Palette', () => {
|
||||
await comfyPage.setSetting('Comfy.CustomColorPalettes', customColorPalettes)
|
||||
// Reload to apply the new setting. Setting Comfy.CustomColorPalettes directly
|
||||
// doesn't update the store immediately.
|
||||
await comfyPage.reload()
|
||||
await comfyPage.setup()
|
||||
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
|
||||
@@ -44,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', () => {
|
||||
|
||||
@@ -369,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)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -593,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')
|
||||
})
|
||||
|
||||
@@ -610,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', () => {
|
||||
|
||||
@@ -21,7 +21,7 @@ test.describe('Menu', () => {
|
||||
expect(await comfyPage.menu.getThemeId()).toBe('light')
|
||||
|
||||
// Theme id should persist after reload.
|
||||
await comfyPage.reload()
|
||||
await comfyPage.setup()
|
||||
expect(await comfyPage.menu.getThemeId()).toBe('light')
|
||||
|
||||
await comfyPage.menu.toggleTheme()
|
||||
@@ -569,7 +569,7 @@ test.describe('Menu', () => {
|
||||
})
|
||||
|
||||
await comfyPage.setSetting('Comfy.Locale', 'zh')
|
||||
await comfyPage.reload()
|
||||
await comfyPage.setup()
|
||||
|
||||
const downloadedContentZh = await comfyPage.getExportedWorkflow({
|
||||
api: false
|
||||
|
||||
@@ -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
2
global.d.ts
vendored
@@ -1 +1,3 @@
|
||||
declare const __COMFYUI_FRONTEND_VERSION__: string
|
||||
declare const __SENTRY_ENABLED__: boolean
|
||||
declare const __SENTRY_DSN__: string
|
||||
|
||||
121
package-lock.json
generated
121
package-lock.json
generated
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.7.4",
|
||||
"version": "1.7.10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.7.4",
|
||||
"version": "1.7.10",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.3",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.7",
|
||||
"@comfyorg/litegraph": "^0.8.60",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
"@tiptap/core": "^2.10.4",
|
||||
"@tiptap/extension-link": "^2.10.4",
|
||||
"@tiptap/extension-table": "^2.10.4",
|
||||
@@ -87,7 +88,8 @@
|
||||
"vite-plugin-static-copy": "^1.0.5",
|
||||
"vitest": "^2.0.5",
|
||||
"vue-tsc": "^2.1.10",
|
||||
"zip-dir": "^2.0.0"
|
||||
"zip-dir": "^2.0.0",
|
||||
"zod-to-json-schema": "^3.24.1"
|
||||
}
|
||||
},
|
||||
"../litegraph.js": {
|
||||
@@ -1934,9 +1936,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@comfyorg/comfyui-electron-types": {
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.3.tgz",
|
||||
"integrity": "sha512-hSM3mchpsYN0e7oZ7XLWjEvFDvE1rgzaB9YkCeqIiZYZgLL78T79ssM0n5ra17Zv7Mqwl6ErZblXvbQE/36RPw==",
|
||||
"version": "0.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.7.tgz",
|
||||
"integrity": "sha512-APC3C4VZOo9W6h0xiAGxnsU9iNp3T8rN9w/5KmOCI0GUoKtKg5U2OaicTmnMwcDSQe5Jxflmej53GyJ1nH9oRw==",
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
@@ -4461,6 +4463,96 @@
|
||||
"string-argv": "~0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/browser-utils": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.48.0.tgz",
|
||||
"integrity": "sha512-pLtu0Fa1Ou0v3M1OEO1MB1EONJVmXEGtoTwFRCO1RPQI2ulmkG6BikINClFG5IBpoYKZ33WkEXuM6U5xh+pdZg==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "8.48.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/feedback": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.48.0.tgz",
|
||||
"integrity": "sha512-6PwcJNHVPg0EfZxmN+XxVOClfQpv7MBAweV8t9i5l7VFr8sM/7wPNSeU/cG7iK19Ug9ZEkBpzMOe3G4GXJ5bpw==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "8.48.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.48.0.tgz",
|
||||
"integrity": "sha512-csILVupc5RkrsTrncuUTGmlB56FQSFjXPYWG8I8yBTGlXEJ+o8oTuF6+55R4vbw3EIzBveXWi4kEBbnQlXW/eg==",
|
||||
"dependencies": {
|
||||
"@sentry-internal/browser-utils": "8.48.0",
|
||||
"@sentry/core": "8.48.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay-canvas": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.48.0.tgz",
|
||||
"integrity": "sha512-LdivLfBXXB9us1aAc6XaL7/L2Ob4vi3C/fEOXElehg3qHjX6q6pewiv5wBvVXGX1NfZTRvu+X11k6TZoxKsezw==",
|
||||
"dependencies": {
|
||||
"@sentry-internal/replay": "8.48.0",
|
||||
"@sentry/core": "8.48.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/browser": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.48.0.tgz",
|
||||
"integrity": "sha512-fuuVULB5/1vI8NoIwXwR3xwhJJqk+y4RdSdajExGF7nnUDBpwUJyXsmYJnOkBO+oLeEs58xaCpotCKiPUNnE3g==",
|
||||
"dependencies": {
|
||||
"@sentry-internal/browser-utils": "8.48.0",
|
||||
"@sentry-internal/feedback": "8.48.0",
|
||||
"@sentry-internal/replay": "8.48.0",
|
||||
"@sentry-internal/replay-canvas": "8.48.0",
|
||||
"@sentry/core": "8.48.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.48.0.tgz",
|
||||
"integrity": "sha512-VGwYgTfLpvJ5LRO5A+qWo1gpo6SfqaGXL9TOzVgBucAdpzbrYHpZ87sEarDVq/4275uk1b0S293/mfsskFczyw==",
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/vue": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/vue/-/vue-8.48.0.tgz",
|
||||
"integrity": "sha512-hqm9X7hz1vMQQB1HBYezrDBQihZk6e/MxWIG1wMJoClcBnD1Sh7y+D36UwaQlR4Gr/Ftiz+Bb0DxuAYHoUS4ow==",
|
||||
"dependencies": {
|
||||
"@sentry/browser": "8.48.0",
|
||||
"@sentry/core": "8.48.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pinia": "2.x",
|
||||
"vue": "2.x || 3.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pinia": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.27.8",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
||||
@@ -19440,21 +19532,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.23.8",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||
"version": "3.24.1",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
|
||||
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zod-to-json-schema": {
|
||||
"version": "3.23.5",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.23.5.tgz",
|
||||
"integrity": "sha512-5wlSS0bXfF/BrL4jPAbz9da5hDlDptdEppYfe+x4eIJ7jioqKG9uUxOwPzqof09u/XeVdrgFu29lZi+8XNDJtA==",
|
||||
"version": "3.24.1",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz",
|
||||
"integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.23.3"
|
||||
"zod": "^3.24.1"
|
||||
}
|
||||
},
|
||||
"node_modules/zod-validation-error": {
|
||||
|
||||
15
package.json
15
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.7.4",
|
||||
"version": "1.7.10",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -17,8 +17,8 @@
|
||||
"update-litegraph": "node scripts/update-litegraph.js",
|
||||
"zipdist": "node scripts/zipdist.js",
|
||||
"typecheck": "vue-tsc --noEmit && tsc --noEmit && tsc-strict",
|
||||
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --plugin @trivago/prettier-plugin-sort-imports",
|
||||
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}' --plugin @trivago/prettier-plugin-sort-imports",
|
||||
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}'",
|
||||
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
|
||||
"test:jest": "jest --config jest.config.ts",
|
||||
"test:generate": "npx tsx tests-ui/setup",
|
||||
"test:browser": "npx playwright test",
|
||||
@@ -28,7 +28,8 @@
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"locale": "lobe-i18n locale",
|
||||
"collect-i18n": "playwright test --config=playwright.i18n.config.ts"
|
||||
"collect-i18n": "playwright test --config=playwright.i18n.config.ts",
|
||||
"json-schema": "tsx scripts/generate-json-schema.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.7",
|
||||
@@ -77,13 +78,15 @@
|
||||
"vite-plugin-static-copy": "^1.0.5",
|
||||
"vitest": "^2.0.5",
|
||||
"vue-tsc": "^2.1.10",
|
||||
"zip-dir": "^2.0.0"
|
||||
"zip-dir": "^2.0.0",
|
||||
"zod-to-json-schema": "^3.24.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.3",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.7",
|
||||
"@comfyorg/litegraph": "^0.8.60",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
"@tiptap/core": "^2.10.4",
|
||||
"@tiptap/extension-link": "^2.10.4",
|
||||
"@tiptap/extension-table": "^2.10.4",
|
||||
|
||||
35
scripts/generate-json-schema.ts
Normal file
35
scripts/generate-json-schema.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema'
|
||||
|
||||
import { zComfyWorkflow, zComfyWorkflow1 } from '../src/types/comfyWorkflow'
|
||||
|
||||
// Convert both workflow schemas to JSON Schema
|
||||
const workflow04Schema = zodToJsonSchema(zComfyWorkflow, {
|
||||
name: 'ComfyWorkflow0_4',
|
||||
$refStrategy: 'none'
|
||||
})
|
||||
|
||||
const workflow1Schema = zodToJsonSchema(zComfyWorkflow1, {
|
||||
name: 'ComfyWorkflow1_0',
|
||||
$refStrategy: 'none'
|
||||
})
|
||||
|
||||
// Create output directory if it doesn't exist
|
||||
const outputDir = './schemas'
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Write schemas to files
|
||||
fs.writeFileSync(
|
||||
path.join(outputDir, 'workflow-0_4.json'),
|
||||
JSON.stringify(workflow04Schema, null, 2)
|
||||
)
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(outputDir, 'workflow-1_0.json'),
|
||||
JSON.stringify(workflow1Schema, null, 2)
|
||||
)
|
||||
|
||||
console.log('JSON Schemas generated successfully!')
|
||||
@@ -10,6 +10,7 @@
|
||||
--bg-color: #fff;
|
||||
--comfy-menu-bg: #353535;
|
||||
--comfy-menu-secondary-bg: #292929;
|
||||
--comfy-topbar-height: 2.5rem;
|
||||
--comfy-input-bg: #222;
|
||||
--input-text: #ddd;
|
||||
--descrip-text: #999;
|
||||
@@ -763,3 +764,17 @@ audio.comfy-audio.empty-audio-widget {
|
||||
.p-tree-node-content {
|
||||
padding: var(--comfy-tree-explorer-item-padding) !important;
|
||||
}
|
||||
|
||||
/* [Desktop] Electron window specific styles */
|
||||
.app-drag {
|
||||
app-region: drag;
|
||||
}
|
||||
|
||||
.no-drag {
|
||||
app-region: no-drag;
|
||||
}
|
||||
|
||||
.window-actions-spacer {
|
||||
width: calc(100vw - env(titlebar-area-width, 100vw));
|
||||
}
|
||||
/* End of [Desktop] Electron window specific styles */
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"NODE_SELECTED_TITLE_COLOR": "#FFF",
|
||||
"NODE_TEXT_SIZE": 14,
|
||||
"NODE_TEXT_COLOR": "#AAA",
|
||||
"NODE_TEXT_HIGHLIGHT_COLOR": "#FFF",
|
||||
"NODE_SUBTEXT_SIZE": 12,
|
||||
"NODE_DEFAULT_COLOR": "#333",
|
||||
"NODE_DEFAULT_BGCOLOR": "#353535",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"NODE_SELECTED_TITLE_COLOR": "#000",
|
||||
"NODE_TEXT_SIZE": 14,
|
||||
"NODE_TEXT_COLOR": "#444",
|
||||
"NODE_TEXT_HIGHLIGHT_COLOR": "#1e293b",
|
||||
"NODE_SUBTEXT_SIZE": 12,
|
||||
"NODE_DEFAULT_COLOR": "#F7F7F7",
|
||||
"NODE_DEFAULT_BGCOLOR": "#F5F5F5",
|
||||
|
||||
@@ -1,18 +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 }"
|
||||
:aria-label="$t('menu.showMenu')"
|
||||
aria-live="assertive"
|
||||
@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">
|
||||
@@ -53,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>
|
||||
|
||||
40
src/components/common/CheckboxGroup.vue
Normal file
40
src/components/common/CheckboxGroup.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div :class="['flex flex-wrap', $attrs.class]">
|
||||
<div
|
||||
v-for="checkbox in checkboxes"
|
||||
:key="checkbox.value"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<Checkbox
|
||||
v-model="internalSelection"
|
||||
:inputId="checkbox.value"
|
||||
:value="checkbox.value"
|
||||
/>
|
||||
<label :for="checkbox.value" class="ml-2">{{ checkbox.label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface CheckboxItem {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
checkboxes: CheckboxItem[]
|
||||
modelValue: string[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string[]): void
|
||||
}>()
|
||||
|
||||
const internalSelection = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: string[]) => emit('update:modelValue', value)
|
||||
})
|
||||
</script>
|
||||
@@ -5,12 +5,20 @@
|
||||
:message="props.error.exception_message"
|
||||
/>
|
||||
<div class="comfy-error-report">
|
||||
<Button
|
||||
v-show="!reportOpen"
|
||||
:label="$t('g.showReport')"
|
||||
@click="showReport"
|
||||
text
|
||||
/>
|
||||
<div class="flex gap-2 justify-center">
|
||||
<Button
|
||||
v-show="!reportOpen"
|
||||
text
|
||||
:label="$t('g.showReport')"
|
||||
@click="showReport"
|
||||
/>
|
||||
<Button
|
||||
v-show="!sendReportOpen"
|
||||
text
|
||||
:label="$t('issueReport.helpFix')"
|
||||
@click="showSendReport"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="reportOpen">
|
||||
<Divider />
|
||||
<ScrollPanel style="width: 100%; height: 400px; max-width: 80vw">
|
||||
@@ -18,9 +26,12 @@
|
||||
</ScrollPanel>
|
||||
<Divider />
|
||||
</template>
|
||||
|
||||
<ReportIssuePanel
|
||||
v-if="sendReportOpen"
|
||||
error-type="graphExecutionError"
|
||||
:extra-fields="[stackTraceField]"
|
||||
/>
|
||||
<div class="action-container">
|
||||
<ReportIssueButton v-if="showSendError" :error="props.error" />
|
||||
<FindIssueButton
|
||||
:errorMessage="props.error.exception_message"
|
||||
:repoOwner="repoOwner"
|
||||
@@ -41,16 +52,18 @@ import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import FindIssueButton from '@/components/dialog/content/error/FindIssueButton.vue'
|
||||
import ReportIssueButton from '@/components/dialog/content/error/ReportIssueButton.vue'
|
||||
import { useCopyToClipboard } from '@/hooks/clipboardHooks'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { ExecutionErrorWsMessage, SystemStats } from '@/types/apiTypes'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
import type { ReportField } from '@/types/issueReportTypes'
|
||||
|
||||
import ReportIssuePanel from './error/ReportIssuePanel.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
error: ExecutionErrorWsMessage
|
||||
@@ -63,9 +76,24 @@ const reportOpen = ref(false)
|
||||
const showReport = () => {
|
||||
reportOpen.value = true
|
||||
}
|
||||
const showSendError = isElectron()
|
||||
|
||||
const sendReportOpen = ref(false)
|
||||
const showSendReport = () => {
|
||||
sendReportOpen.value = true
|
||||
}
|
||||
const toast = useToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
const stackTraceField = computed<ReportField>(() => {
|
||||
return {
|
||||
label: t('issueReport.stackTrace'),
|
||||
value: 'StackTrace',
|
||||
optIn: true,
|
||||
data: {
|
||||
nodeType: props.error.node_type,
|
||||
stackTrace: props.error.traceback?.join('\n')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
|
||||
@@ -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>
|
||||
197
src/components/dialog/content/error/ReportIssuePanel.vue
Normal file
197
src/components/dialog/content/error/ReportIssuePanel.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<Panel>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-bold">{{ $t('issueReport.submitErrorReport') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
v-tooltip="$t('g.reportIssueTooltip')"
|
||||
:label="submitted ? $t('g.reportSent') : $t('g.reportIssue')"
|
||||
:severity="isButtonDisabled ? 'secondary' : 'primary'"
|
||||
:icon="icon"
|
||||
:disabled="isButtonDisabled"
|
||||
@click="reportIssue"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="p-4 mt-4 border border-round surface-border shadow-1">
|
||||
<CheckboxGroup
|
||||
v-model="selection"
|
||||
class="gap-4 mb-4"
|
||||
:checkboxes="reportCheckboxes"
|
||||
/>
|
||||
<div class="mb-4">
|
||||
<InputText
|
||||
v-model="contactInfo"
|
||||
class="w-full"
|
||||
:placeholder="$t('issueReport.provideEmail')"
|
||||
:maxlength="CONTACT_MAX_LEN"
|
||||
/>
|
||||
<CheckboxGroup
|
||||
v-model="contactPrefs"
|
||||
class="gap-3 mt-2"
|
||||
:checkboxes="contactCheckboxes"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<Textarea
|
||||
v-model="details"
|
||||
class="w-full"
|
||||
rows="4"
|
||||
:maxlength="DETAILS_MAX_LEN"
|
||||
:placeholder="$t('issueReport.provideAdditionalDetails')"
|
||||
:aria-label="$t('issueReport.provideAdditionalDetails')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CaptureContext, User } from '@sentry/core'
|
||||
import { captureMessage } from '@sentry/core'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Panel from 'primevue/panel'
|
||||
import Textarea from 'primevue/textarea'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import CheckboxGroup from '@/components/common/CheckboxGroup.vue'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { DefaultField, ReportField } from '@/types/issueReportTypes'
|
||||
|
||||
const ISSUE_NAME = 'User reported issue'
|
||||
const DETAILS_MAX_LEN = 5_000
|
||||
const CONTACT_MAX_LEN = 320
|
||||
|
||||
const props = defineProps<{
|
||||
errorType: string
|
||||
defaultFields?: DefaultField[]
|
||||
extraFields?: ReportField[]
|
||||
}>()
|
||||
const { defaultFields = ['Workflow', 'Logs', 'SystemStats', 'Settings'] } =
|
||||
props
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const selection = ref<string[]>([])
|
||||
const contactPrefs = ref<string[]>([])
|
||||
const contactInfo = ref('')
|
||||
const details = ref('')
|
||||
const submitting = ref(false)
|
||||
const submitted = ref(false)
|
||||
|
||||
const followUp = computed(() => contactPrefs.value.includes('FollowUp'))
|
||||
const notifyResolve = computed(() => contactPrefs.value.includes('Resolution'))
|
||||
|
||||
const icon = computed(() => {
|
||||
if (submitting.value) return 'pi pi-spin pi-spinner'
|
||||
if (submitted.value) return 'pi pi-check'
|
||||
return 'pi pi-send'
|
||||
})
|
||||
const isFormEmpty = computed(() => !selection.value.length && !details.value)
|
||||
const isButtonDisabled = computed(
|
||||
() => submitted.value || submitting.value || isFormEmpty.value
|
||||
)
|
||||
|
||||
const contactCheckboxes = [
|
||||
{ label: t('issueReport.contactFollowUp'), value: 'FollowUp' },
|
||||
{ label: t('issueReport.notifyResolve'), value: 'Resolution' }
|
||||
]
|
||||
const defaultReportCheckboxes = [
|
||||
{ label: t('g.workflow'), value: 'Workflow' },
|
||||
{ label: t('g.logs'), value: 'Logs' },
|
||||
{ label: t('issueReport.systemStats'), value: 'SystemStats' },
|
||||
{ label: t('g.settings'), value: 'Settings' }
|
||||
]
|
||||
const reportCheckboxes = computed(() => [
|
||||
...(props.extraFields?.map(({ label, value }) => ({ label, value })) ?? []),
|
||||
...defaultReportCheckboxes.filter(({ value }) =>
|
||||
defaultFields.includes(value as DefaultField)
|
||||
)
|
||||
])
|
||||
|
||||
const getUserInfo = (): User => ({ email: contactInfo.value })
|
||||
|
||||
const getLogs = async () =>
|
||||
selection.value.includes('Logs') ? api.getLogs() : null
|
||||
|
||||
const getSystemStats = async () =>
|
||||
selection.value.includes('SystemStats') ? api.getSystemStats() : null
|
||||
|
||||
const getSettings = async () =>
|
||||
selection.value.includes('Settings') ? api.getSettings() : null
|
||||
|
||||
const getWorkflow = () =>
|
||||
selection.value.includes('Workflow')
|
||||
? cloneDeep(app.graph.asSerialisable())
|
||||
: null
|
||||
|
||||
const createDefaultFields = async () => {
|
||||
const [settings, systemStats, logs, workflow] = await Promise.all([
|
||||
getSettings(),
|
||||
getSystemStats(),
|
||||
getLogs(),
|
||||
getWorkflow()
|
||||
])
|
||||
return { settings, systemStats, logs, workflow }
|
||||
}
|
||||
|
||||
const createExtraFields = (): Record<string, unknown> | undefined => {
|
||||
if (!props.extraFields) return undefined
|
||||
|
||||
return props.extraFields
|
||||
.filter((field) => !field.optIn || selection.value.includes(field.value))
|
||||
.reduce((acc, field) => ({ ...acc, ...cloneDeep(field.data) }), {})
|
||||
}
|
||||
|
||||
const createFeedback = () => {
|
||||
return {
|
||||
details: details.value,
|
||||
contactPreferences: {
|
||||
followUp: followUp.value,
|
||||
notifyOnResolution: notifyResolve.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createCaptureContext = async (): Promise<CaptureContext> => {
|
||||
return {
|
||||
user: getUserInfo(),
|
||||
level: 'error',
|
||||
tags: {
|
||||
errorType: props.errorType
|
||||
},
|
||||
extra: {
|
||||
...createFeedback(),
|
||||
...(await createDefaultFields()),
|
||||
...createExtraFields()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const reportIssue = async () => {
|
||||
if (isButtonDisabled.value) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
captureMessage(ISSUE_NAME, await createCaptureContext())
|
||||
submitted.value = true
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('g.reportSent'),
|
||||
life: 3000
|
||||
})
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,230 @@
|
||||
// @ts-strict-ignore
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Button from 'primevue/button'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Panel from 'primevue/panel'
|
||||
import Textarea from 'primevue/textarea'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { createApp } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import CheckboxGroup from '@/components/common/CheckboxGroup.vue'
|
||||
import enMesages from '@/locales/en/main.json'
|
||||
import { DefaultField, ReportField } from '@/types/issueReportTypes'
|
||||
|
||||
import ReportIssuePanel from '../ReportIssuePanel.vue'
|
||||
|
||||
type ReportIssuePanelProps = {
|
||||
errorType: string
|
||||
defaultFields?: DefaultField[]
|
||||
extraFields?: ReportField[]
|
||||
}
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: enMesages
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('primevue/usetoast', () => ({
|
||||
useToast: vi.fn(() => ({
|
||||
add: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
getLogs: vi.fn().mockResolvedValue('mock logs'),
|
||||
getSystemStats: vi.fn().mockResolvedValue('mock stats'),
|
||||
getSettings: vi.fn().mockResolvedValue('mock settings')
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
graph: {
|
||||
asSerialisable: vi.fn().mockReturnValue({})
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@sentry/core', () => ({
|
||||
captureMessage: vi.fn()
|
||||
}))
|
||||
|
||||
describe('ReportIssuePanel', () => {
|
||||
beforeAll(() => {
|
||||
const app = createApp({})
|
||||
app.use(PrimeVue)
|
||||
})
|
||||
|
||||
const mountComponent = (props: ReportIssuePanelProps, options = {}): any => {
|
||||
return mount(ReportIssuePanel, {
|
||||
global: {
|
||||
plugins: [PrimeVue, createTestingPinia(), i18n],
|
||||
directives: { tooltip: Tooltip },
|
||||
components: { InputText, Button, Panel, Textarea, CheckboxGroup }
|
||||
},
|
||||
props,
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
it('renders the panel with all required components', () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
expect(wrapper.find('.p-panel').exists()).toBe(true)
|
||||
expect(wrapper.findAllComponents(CheckboxGroup).length).toBe(2)
|
||||
expect(wrapper.findComponent(InputText).exists()).toBe(true)
|
||||
expect(wrapper.findComponent(Textarea).exists()).toBe(true)
|
||||
expect(wrapper.findComponent(Button).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('updates selection when checkboxes are selected', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
|
||||
await checkboxes?.setValue(['Workflow', 'Logs'])
|
||||
expect(wrapper.vm.selection).toEqual(['Workflow', 'Logs'])
|
||||
})
|
||||
|
||||
it('updates contactInfo when input is changed', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
const input = wrapper.findComponent(InputText)
|
||||
await input.setValue('test@example.com')
|
||||
expect(wrapper.vm.contactInfo).toBe('test@example.com')
|
||||
})
|
||||
|
||||
it('updates additional details when textarea is changed', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
const textarea = wrapper.findComponent(Textarea)
|
||||
await textarea.setValue('This is a test detail.')
|
||||
expect(wrapper.vm.details).toBe('This is a test detail.')
|
||||
})
|
||||
|
||||
it('updates contactPrefs when preferences are selected', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
const preferences = wrapper.findAllComponents(CheckboxGroup).at(1)
|
||||
await preferences?.setValue(['FollowUp'])
|
||||
expect(wrapper.vm.contactPrefs).toEqual(['FollowUp'])
|
||||
})
|
||||
|
||||
it('does not allow submission if the form is empty', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
await wrapper.vm.reportIssue()
|
||||
expect(wrapper.vm.submitted).toBe(false)
|
||||
})
|
||||
|
||||
it('renders with overridden default fields', () => {
|
||||
const wrapper = mountComponent({
|
||||
errorType: 'Test Error',
|
||||
defaultFields: ['Settings']
|
||||
})
|
||||
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
|
||||
expect(checkboxes?.props('checkboxes')).toEqual([
|
||||
{ label: 'Settings', value: 'Settings' }
|
||||
])
|
||||
})
|
||||
|
||||
it('renders additional fields when extraFields prop is provided', () => {
|
||||
const extraFields = [
|
||||
{ label: 'Custom Field', value: 'CustomField', optIn: true, data: {} }
|
||||
]
|
||||
const wrapper = mountComponent({ errorType: 'Test Error', extraFields })
|
||||
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
|
||||
expect(checkboxes?.props('checkboxes')).toContainEqual({
|
||||
label: 'Custom Field',
|
||||
value: 'CustomField'
|
||||
})
|
||||
})
|
||||
|
||||
it('does not submit unchecked fields', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
const textarea = wrapper.findComponent(Textarea)
|
||||
|
||||
await textarea.setValue('Report with only text but no fields selected')
|
||||
await wrapper.vm.reportIssue()
|
||||
|
||||
const { captureMessage } = (await import('@sentry/core')) as any
|
||||
const captureContext = captureMessage.mock.calls[0][1]
|
||||
|
||||
expect(captureContext.extra.logs).toBeNull()
|
||||
expect(captureContext.extra.systemStats).toBeNull()
|
||||
expect(captureContext.extra.settings).toBeNull()
|
||||
expect(captureContext.extra.workflow).toBeNull()
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
checkbox: 'Logs',
|
||||
apiMethod: 'getLogs',
|
||||
expectedKey: 'logs',
|
||||
mockValue: 'mock logs'
|
||||
},
|
||||
{
|
||||
checkbox: 'SystemStats',
|
||||
apiMethod: 'getSystemStats',
|
||||
expectedKey: 'systemStats',
|
||||
mockValue: 'mock stats'
|
||||
},
|
||||
{
|
||||
checkbox: 'Settings',
|
||||
apiMethod: 'getSettings',
|
||||
expectedKey: 'settings',
|
||||
mockValue: 'mock settings'
|
||||
}
|
||||
])(
|
||||
'submits (%s) when the (%s) checkbox is selected',
|
||||
async ({ checkbox, apiMethod, expectedKey, mockValue }) => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
|
||||
const { api } = (await import('@/scripts/api')) as any
|
||||
vi.spyOn(api, apiMethod).mockResolvedValue(mockValue)
|
||||
|
||||
const { captureMessage } = await import('@sentry/core')
|
||||
|
||||
// Select the checkbox
|
||||
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
|
||||
await checkboxes?.vm.$emit('update:modelValue', [checkbox])
|
||||
|
||||
await wrapper.vm.reportIssue()
|
||||
expect(api[apiMethod]).toHaveBeenCalled()
|
||||
|
||||
// Verify the message includes the associated data
|
||||
expect(captureMessage).toHaveBeenCalledWith(
|
||||
'User reported issue',
|
||||
expect.objectContaining({
|
||||
extra: expect.objectContaining({ [expectedKey]: mockValue })
|
||||
})
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
it('submits workflow when the Workflow checkbox is selected', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
|
||||
const { app } = (await import('@/scripts/app')) as any
|
||||
const { captureMessage } = await import('@sentry/core')
|
||||
|
||||
const mockWorkflow = { nodes: [], edges: [] }
|
||||
vi.spyOn(app.graph, 'asSerialisable').mockReturnValue(mockWorkflow)
|
||||
|
||||
// Select the "Workflow" checkbox
|
||||
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
|
||||
await checkboxes?.vm.$emit('update:modelValue', ['Workflow'])
|
||||
|
||||
await wrapper.vm.reportIssue()
|
||||
expect(app.graph.asSerialisable).toHaveBeenCalled()
|
||||
|
||||
// Verify the message includes the workflow
|
||||
expect(captureMessage).toHaveBeenCalledWith(
|
||||
'User reported issue',
|
||||
expect.objectContaining({
|
||||
extra: expect.objectContaining({ workflow: mockWorkflow })
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -13,13 +13,13 @@
|
||||
optionValue="id"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-download"
|
||||
icon="pi pi-upload"
|
||||
text
|
||||
:title="$t('g.download')"
|
||||
:title="$t('g.export')"
|
||||
@click="colorPaletteService.exportColorPalette(activePaletteId)"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-upload"
|
||||
icon="pi pi-download"
|
||||
text
|
||||
:title="$t('g.import')"
|
||||
@click="importCustomPalette"
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
<BottomPanel />
|
||||
</template>
|
||||
<template #graph-canvas-panel>
|
||||
<SecondRowWorkflowTabs
|
||||
v-if="workflowTabsPosition === 'Topbar (2nd-row)'"
|
||||
/>
|
||||
<GraphCanvasMenu v-if="canvasMenuEnabled" />
|
||||
</template>
|
||||
</LiteGraphCanvasSplitterOverlay>
|
||||
@@ -48,12 +51,13 @@ import NodeTooltip from '@/components/graph/NodeTooltip.vue'
|
||||
import TitleEditor from '@/components/graph/TitleEditor.vue'
|
||||
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
|
||||
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
|
||||
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
|
||||
import { CORE_SETTINGS } from '@/constants/coreSettings'
|
||||
import { usePragmaticDroppable } from '@/hooks/dndHooks'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||
import { setStorageValue } from '@/scripts/utils'
|
||||
import { getStorageValue, setStorageValue } from '@/scripts/utils'
|
||||
import { IS_CONTROL_WIDGET, updateControlWidgetLabel } from '@/scripts/widgets'
|
||||
import { useColorPaletteService } from '@/services/colorPaletteService'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
@@ -83,11 +87,37 @@ const modelToNodeStore = useModelToNodeStore()
|
||||
const betaMenuEnabled = computed(
|
||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
const workflowTabsPosition = computed(() =>
|
||||
settingStore.get('Comfy.Workflow.WorkflowTabsPosition')
|
||||
)
|
||||
const canvasMenuEnabled = computed(() =>
|
||||
settingStore.get('Comfy.Graph.CanvasMenu')
|
||||
)
|
||||
const tooltipEnabled = computed(() => settingStore.get('Comfy.EnableTooltips'))
|
||||
|
||||
const storedWorkflows = JSON.parse(
|
||||
getStorageValue('Comfy.OpenWorkflowsPaths') || '[]'
|
||||
)
|
||||
const storedActiveIndex = JSON.parse(
|
||||
getStorageValue('Comfy.ActiveWorkflowIndex') || '-1'
|
||||
)
|
||||
const openWorkflows = computed(() => workspaceStore?.workflow?.openWorkflows)
|
||||
const activeWorkflow = computed(() => workspaceStore?.workflow?.activeWorkflow)
|
||||
const restoreState = computed<{ paths: string[]; activeIndex: number }>(() => {
|
||||
if (!openWorkflows.value || !activeWorkflow.value) {
|
||||
return { paths: [], activeIndex: -1 }
|
||||
}
|
||||
|
||||
const paths = openWorkflows.value
|
||||
.filter((workflow) => workflow?.isPersisted && !workflow.isModified)
|
||||
.map((workflow) => workflow.path)
|
||||
const activeIndex = openWorkflows.value.findIndex(
|
||||
(workflow) => workflow.path === activeWorkflow.value?.path
|
||||
)
|
||||
|
||||
return { paths, activeIndex }
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
const canvasInfoEnabled = settingStore.get('Comfy.Graph.CanvasInfo')
|
||||
if (canvasStore.canvas) {
|
||||
@@ -357,6 +387,18 @@ onMounted(async () => {
|
||||
'Comfy.CustomColorPalettes'
|
||||
)
|
||||
|
||||
const isRestorable = storedWorkflows?.length > 0 && storedActiveIndex >= 0
|
||||
if (isRestorable)
|
||||
workflowStore.openWorkflowsInBackground({
|
||||
left: storedWorkflows.slice(0, storedActiveIndex),
|
||||
right: storedWorkflows.slice(storedActiveIndex)
|
||||
})
|
||||
|
||||
watch(restoreState, ({ paths, activeIndex }) => {
|
||||
setStorageValue('Comfy.OpenWorkflowsPaths', JSON.stringify(paths))
|
||||
setStorageValue('Comfy.ActiveWorkflowIndex', JSON.stringify(activeIndex))
|
||||
})
|
||||
|
||||
// Start watching for locale change after the initial value is loaded.
|
||||
watch(
|
||||
() => settingStore.get('Comfy.Locale'),
|
||||
|
||||
@@ -59,10 +59,27 @@
|
||||
</h4>
|
||||
<ul class="list-disc pl-6 space-y-1">
|
||||
<li>
|
||||
{{ $t('install.settings.dataCollectionDialog.errorReports') }}
|
||||
{{
|
||||
$t('install.settings.dataCollectionDialog.collect.errorReports')
|
||||
}}
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('install.settings.dataCollectionDialog.systemInfo') }}
|
||||
{{ $t('install.settings.dataCollectionDialog.collect.systemInfo') }}
|
||||
</li>
|
||||
<li>
|
||||
{{
|
||||
$t(
|
||||
'install.settings.dataCollectionDialog.collect.userJourneyEvents'
|
||||
)
|
||||
}}
|
||||
<span
|
||||
class="pi pi-info-circle text-neutral-400"
|
||||
v-tooltip="
|
||||
$t(
|
||||
'install.settings.dataCollectionDialog.collect.userJourneyTooltip'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -72,21 +89,29 @@
|
||||
<ul class="list-disc pl-6 space-y-1">
|
||||
<li>
|
||||
{{
|
||||
$t('install.settings.dataCollectionDialog.personalInformation')
|
||||
}}
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('install.settings.dataCollectionDialog.workflowContents') }}
|
||||
</li>
|
||||
<li>
|
||||
{{
|
||||
$t('install.settings.dataCollectionDialog.fileSystemInformation')
|
||||
$t(
|
||||
'install.settings.dataCollectionDialog.doNotCollect.personalInformation'
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
<li>
|
||||
{{
|
||||
$t(
|
||||
'install.settings.dataCollectionDialog.customNodeConfigurations'
|
||||
'install.settings.dataCollectionDialog.doNotCollect.workflowContents'
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
<li>
|
||||
{{
|
||||
$t(
|
||||
'install.settings.dataCollectionDialog.doNotCollect.fileSystemInformation'
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
<li>
|
||||
{{
|
||||
$t(
|
||||
'install.settings.dataCollectionDialog.doNotCollect.customNodeConfigurations'
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -19,7 +19,13 @@
|
||||
class="filter-button z-10"
|
||||
@click="nodeSearchFilterVisible = true"
|
||||
/>
|
||||
<Dialog v-model:visible="nodeSearchFilterVisible" class="min-w-96">
|
||||
<Dialog
|
||||
v-model:visible="nodeSearchFilterVisible"
|
||||
class="min-w-96"
|
||||
dismissable-mask
|
||||
modal
|
||||
@hide="reFocusInput"
|
||||
>
|
||||
<template #header>
|
||||
<h3>Add node filter condition</h3>
|
||||
</template>
|
||||
@@ -140,7 +146,6 @@ onMounted(reFocusInput)
|
||||
const onAddFilter = (filterAndValue: FilterAndValue) => {
|
||||
nodeSearchFilterVisible.value = false
|
||||
emit('addFilter', filterAndValue)
|
||||
reFocusInput()
|
||||
}
|
||||
const onRemoveFilter = (event: Event, filterAndValue: FilterAndValue) => {
|
||||
event.stopPropagation()
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="showCategory"
|
||||
class="option-category font-light text-sm text-gray-400 overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
class="option-category font-light text-sm text-muted overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
{{ nodeDef.category.replaceAll('/', ' > ') }}
|
||||
</div>
|
||||
|
||||
@@ -71,11 +71,11 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--sidebar-width: 64px;
|
||||
--sidebar-width: 4rem;
|
||||
--sidebar-icon-size: 1.5rem;
|
||||
}
|
||||
:root .small-sidebar {
|
||||
--sidebar-width: 40px;
|
||||
--sidebar-width: 2.5rem;
|
||||
--sidebar-icon-size: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,6 +20,14 @@
|
||||
@click="alphabeticalSort = !alphabeticalSort"
|
||||
v-tooltip.bottom="$t('sideToolbar.nodeLibraryTab.sortOrder')"
|
||||
/>
|
||||
<Button
|
||||
class="grouping-button"
|
||||
:icon="groupBySource ? 'pi pi-list' : 'pi pi-list-check'"
|
||||
text
|
||||
severity="secondary"
|
||||
@click="groupBySource = !groupBySource"
|
||||
v-tooltip.bottom="$t('sideToolbar.nodeLibraryTab.groupingType')"
|
||||
/>
|
||||
</template>
|
||||
<template #header>
|
||||
<SearchBox
|
||||
@@ -66,7 +74,7 @@ import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import Popover from 'primevue/popover'
|
||||
import type { TreeNode } from 'primevue/treenode'
|
||||
import { Ref, computed, nextTick, ref } from 'vue'
|
||||
import { Ref, computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import { SearchFilter } from '@/components/common/SearchFilterChip.vue'
|
||||
@@ -101,6 +109,21 @@ const nodeBookmarkTreeExplorerRef = ref<InstanceType<
|
||||
> | null>(null)
|
||||
const searchFilter = ref(null)
|
||||
const alphabeticalSort = ref(false)
|
||||
const groupBySource = ref(false)
|
||||
|
||||
const createSourceKey = (nodeDef: ComfyNodeDefImpl) => {
|
||||
const sourcePath = nodeDef.python_module.split('.')
|
||||
const pathWithoutCategory = nodeDef.nodePath.split('/').slice(1)
|
||||
return [...sourcePath, ...pathWithoutCategory]
|
||||
}
|
||||
|
||||
watch(groupBySource, (newValue) => {
|
||||
if (newValue) {
|
||||
nodeDefStore.setKeyFunction(createSourceKey)
|
||||
} else {
|
||||
nodeDefStore.setKeyFunction(null)
|
||||
}
|
||||
})
|
||||
|
||||
const searchQuery = ref<string>('')
|
||||
|
||||
@@ -194,4 +217,9 @@ const onRemoveFilter = (filterAndValue) => {
|
||||
}
|
||||
handleSearch(searchQuery.value)
|
||||
}
|
||||
|
||||
// This can be added if the persistent state is not desirable:
|
||||
// onBeforeUnmount(() => {
|
||||
// nodeDefStore.setKeyFunction(null)
|
||||
// })
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<template #header>
|
||||
<div class="flex items-center justify-center">
|
||||
<div
|
||||
class="relative overflow-hidden rounded-lg cursor-pointer w-64 h-64"
|
||||
class="relative overflow-hidden rounded-t-lg cursor-pointer w-64 h-64"
|
||||
>
|
||||
<img
|
||||
v-if="!imageError"
|
||||
@@ -13,7 +13,7 @@
|
||||
: `api/workflow_templates/${props.moduleName}/${props.workflowName}.jpg`
|
||||
"
|
||||
@error="imageError = true"
|
||||
class="w-64 h-64 rounded-lg object-cover thumbnail"
|
||||
class="w-64 h-64 rounded-t-lg object-cover thumbnail"
|
||||
/>
|
||||
<div v-else class="w-64 h-64 content-center text-center">
|
||||
<i class="pi pi-file" style="font-size: 4rem"></i>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
:key="selectedTab.moduleName"
|
||||
>
|
||||
<template #item="slotProps">
|
||||
<div @click="loadWorkflow(slotProps.data)">
|
||||
<div @click="loadWorkflow(slotProps.data)" class="p-2">
|
||||
<TemplateWorkflowCard
|
||||
:moduleName="selectedTab.moduleName"
|
||||
:workflowName="slotProps.data"
|
||||
|
||||
15
src/components/topbar/SecondRowWorkflowTabs.vue
Normal file
15
src/components/topbar/SecondRowWorkflowTabs.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="absolute top-0 left-0 w-auto max-w-full pointer-events-auto">
|
||||
<WorkflowTabs />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.workflow-tabs) {
|
||||
background-color: var(--comfy-menu-bg);
|
||||
}
|
||||
</style>
|
||||
@@ -3,19 +3,19 @@
|
||||
<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
|
||||
@@ -24,14 +24,23 @@
|
||||
@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'
|
||||
@@ -41,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
|
||||
@@ -76,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);
|
||||
@@ -90,7 +114,6 @@ eventBus.on((event: string, payload: any) => {
|
||||
z-index: 1000;
|
||||
order: 0;
|
||||
grid-column: 1/-1;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.comfyui-menu.dropzone {
|
||||
|
||||
@@ -1,36 +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
|
||||
v-tooltip="{ value: $t('sideToolbar.newBlankWorkflow'), showDelay: 300 }"
|
||||
class="new-blank-workflow-button"
|
||||
icon="pi pi-plus"
|
||||
text
|
||||
severity="secondary"
|
||||
:aria-label="$t('sideToolbar.newBlankWorkflow')"
|
||||
@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'
|
||||
@@ -38,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'
|
||||
|
||||
@@ -55,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()
|
||||
|
||||
@@ -142,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;
|
||||
@@ -174,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>
|
||||
|
||||
@@ -85,7 +85,8 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
name: 'Sidebar size',
|
||||
type: 'combo',
|
||||
options: ['normal', 'small'],
|
||||
defaultValue: () => (window.innerWidth < 1600 ? 'small' : 'normal')
|
||||
// Default to small if the window is less than 1536px(2xl) wide.
|
||||
defaultValue: () => (window.innerWidth < 1536 ? 'small' : 'normal')
|
||||
},
|
||||
{
|
||||
id: 'Comfy.TextareaWidget.FontSize',
|
||||
@@ -399,8 +400,10 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
id: 'Comfy.Workflow.WorkflowTabsPosition',
|
||||
name: 'Opened workflows position',
|
||||
type: 'combo',
|
||||
options: ['Sidebar', 'Topbar'],
|
||||
defaultValue: 'Topbar'
|
||||
options: ['Sidebar', 'Topbar', 'Topbar (2nd-row)'],
|
||||
// Default to topbar (2nd-row) if the window is less than 1536px(2xl) wide.
|
||||
defaultValue: () =>
|
||||
window.innerWidth < 1536 ? 'Topbar (2nd-row)' : 'Topbar'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.CanvasMenu',
|
||||
@@ -423,7 +426,16 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
name: 'Keybindings unset by the user',
|
||||
type: 'hidden',
|
||||
defaultValue: [] as Keybinding[],
|
||||
versionAdded: '1.3.7'
|
||||
versionAdded: '1.3.7',
|
||||
versionModified: '1.7.3',
|
||||
migrateDeprecatedValue: (value: any[]) => {
|
||||
return value.map((keybinding) => {
|
||||
if (keybinding['targetSelector'] === '#graph-canvas') {
|
||||
keybinding['targetElementId'] = 'graph-canvas'
|
||||
}
|
||||
return keybinding
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Keybinding.NewBindings',
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { IWidget } from '@comfyorg/litegraph'
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
|
||||
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
|
||||
import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
|
||||
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
|
||||
@@ -115,8 +116,7 @@ class Load3d {
|
||||
stlLoader: STLLoader
|
||||
currentModel: THREE.Object3D | null = null
|
||||
originalModel: THREE.Object3D | THREE.BufferGeometry | GLTF | null = null
|
||||
node: any
|
||||
private animationFrameId: number | null = null
|
||||
animationFrameId: number | null = null
|
||||
gridHelper: THREE.GridHelper
|
||||
lights: THREE.Light[] = []
|
||||
clock: THREE.Clock
|
||||
@@ -131,6 +131,10 @@ class Load3d {
|
||||
currentUpDirection: 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z' =
|
||||
'original'
|
||||
originalRotation: THREE.Euler | null = null
|
||||
viewHelper: ViewHelper
|
||||
viewHelperContainer: HTMLDivElement
|
||||
cameraSwitcherContainer: HTMLDivElement
|
||||
gridSwitcherContainer: HTMLDivElement
|
||||
|
||||
constructor(container: Element | HTMLElement) {
|
||||
this.scene = new THREE.Scene()
|
||||
@@ -157,6 +161,7 @@ class Load3d {
|
||||
this.renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true })
|
||||
this.renderer.setSize(300, 300)
|
||||
this.renderer.setClearColor(0x282828)
|
||||
this.renderer.autoClear = false
|
||||
|
||||
const rendererDomElement: HTMLCanvasElement = this.renderer.domElement
|
||||
|
||||
@@ -203,13 +208,143 @@ class Load3d {
|
||||
|
||||
this.standardMaterial = this.createSTLMaterial()
|
||||
|
||||
this.animate()
|
||||
this.createViewHelper(container)
|
||||
|
||||
this.createGridSwitcher(container)
|
||||
|
||||
this.createCameraSwitcher(container)
|
||||
|
||||
this.handleResize()
|
||||
|
||||
this.startAnimation()
|
||||
}
|
||||
|
||||
createViewHelper(container: Element | HTMLElement) {
|
||||
this.viewHelperContainer = document.createElement('div')
|
||||
|
||||
this.viewHelperContainer.style.position = 'absolute'
|
||||
this.viewHelperContainer.style.bottom = '0'
|
||||
this.viewHelperContainer.style.left = '0'
|
||||
this.viewHelperContainer.style.width = '128px'
|
||||
this.viewHelperContainer.style.height = '128px'
|
||||
this.viewHelperContainer.addEventListener('pointerup', (event) => {
|
||||
event.stopPropagation()
|
||||
|
||||
this.viewHelper.handleClick(event)
|
||||
})
|
||||
|
||||
this.viewHelperContainer.addEventListener('pointerdown', (event) => {
|
||||
event.stopPropagation()
|
||||
})
|
||||
|
||||
container.appendChild(this.viewHelperContainer)
|
||||
|
||||
this.viewHelper = new ViewHelper(
|
||||
this.activeCamera,
|
||||
this.viewHelperContainer
|
||||
)
|
||||
|
||||
this.viewHelper.center = this.controls.target
|
||||
}
|
||||
|
||||
createGridSwitcher(container: Element | HTMLElement) {
|
||||
this.gridSwitcherContainer = document.createElement('div')
|
||||
this.gridSwitcherContainer.style.position = 'absolute'
|
||||
this.gridSwitcherContainer.style.top = '28px' // 修改这里,让按钮在相机按钮下方
|
||||
this.gridSwitcherContainer.style.left = '3px' // 与相机按钮左对齐
|
||||
this.gridSwitcherContainer.style.width = '20px'
|
||||
this.gridSwitcherContainer.style.height = '20px'
|
||||
this.gridSwitcherContainer.style.cursor = 'pointer'
|
||||
this.gridSwitcherContainer.style.alignItems = 'center'
|
||||
this.gridSwitcherContainer.style.justifyContent = 'center'
|
||||
this.gridSwitcherContainer.style.transition = 'background-color 0.2s'
|
||||
|
||||
const gridIcon = document.createElement('div')
|
||||
gridIcon.innerHTML = `
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
|
||||
<path d="M3 3h18v18H3z"/>
|
||||
<path d="M3 9h18"/>
|
||||
<path d="M3 15h18"/>
|
||||
<path d="M9 3v18"/>
|
||||
<path d="M15 3v18"/>
|
||||
</svg>
|
||||
`
|
||||
|
||||
const updateButtonState = () => {
|
||||
if (this.gridHelper.visible) {
|
||||
this.gridSwitcherContainer.style.backgroundColor =
|
||||
'rgba(255, 255, 255, 0.2)'
|
||||
} else {
|
||||
this.gridSwitcherContainer.style.backgroundColor = 'transparent'
|
||||
}
|
||||
}
|
||||
|
||||
updateButtonState()
|
||||
|
||||
this.gridSwitcherContainer.addEventListener('mouseenter', () => {
|
||||
if (!this.gridHelper.visible) {
|
||||
this.gridSwitcherContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
})
|
||||
|
||||
this.gridSwitcherContainer.addEventListener('mouseleave', () => {
|
||||
if (!this.gridHelper.visible) {
|
||||
this.gridSwitcherContainer.style.backgroundColor = 'transparent'
|
||||
}
|
||||
})
|
||||
|
||||
this.gridSwitcherContainer.title = 'Toggle Grid'
|
||||
|
||||
this.gridSwitcherContainer.addEventListener('click', (event) => {
|
||||
event.stopPropagation()
|
||||
this.toggleGrid(!this.gridHelper.visible)
|
||||
updateButtonState()
|
||||
})
|
||||
|
||||
this.gridSwitcherContainer.appendChild(gridIcon)
|
||||
container.appendChild(this.gridSwitcherContainer)
|
||||
}
|
||||
|
||||
createCameraSwitcher(container: Element | HTMLElement) {
|
||||
this.cameraSwitcherContainer = document.createElement('div')
|
||||
this.cameraSwitcherContainer.style.position = 'absolute'
|
||||
this.cameraSwitcherContainer.style.top = '3px'
|
||||
this.cameraSwitcherContainer.style.left = '3px'
|
||||
this.cameraSwitcherContainer.style.width = '20px'
|
||||
this.cameraSwitcherContainer.style.height = '20px'
|
||||
this.cameraSwitcherContainer.style.cursor = 'pointer'
|
||||
this.cameraSwitcherContainer.style.alignItems = 'center'
|
||||
this.cameraSwitcherContainer.style.justifyContent = 'center'
|
||||
|
||||
const cameraIcon = document.createElement('div')
|
||||
cameraIcon.innerHTML = `
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
|
||||
<path d="M18 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2Z"/>
|
||||
<path d="m12 12 4-2.4"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</svg>
|
||||
`
|
||||
this.cameraSwitcherContainer.addEventListener('mouseenter', () => {
|
||||
this.cameraSwitcherContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
|
||||
})
|
||||
|
||||
this.cameraSwitcherContainer.addEventListener('mouseleave', () => {
|
||||
this.cameraSwitcherContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.3)'
|
||||
})
|
||||
|
||||
this.cameraSwitcherContainer.title =
|
||||
'Switch Camera (Perspective/Orthographic)'
|
||||
|
||||
this.cameraSwitcherContainer.addEventListener('click', (event) => {
|
||||
event.stopPropagation()
|
||||
this.toggleCamera()
|
||||
})
|
||||
|
||||
this.cameraSwitcherContainer.appendChild(cameraIcon)
|
||||
|
||||
container.appendChild(this.cameraSwitcherContainer)
|
||||
}
|
||||
|
||||
setFOV(fov: number) {
|
||||
if (this.activeCamera === this.perspectiveCamera) {
|
||||
this.perspectiveCamera.fov = fov
|
||||
@@ -465,6 +600,13 @@ class Load3d {
|
||||
this.controls.target.copy(target)
|
||||
this.controls.update()
|
||||
|
||||
this.viewHelper.dispose()
|
||||
this.viewHelper = new ViewHelper(
|
||||
this.activeCamera,
|
||||
this.viewHelperContainer
|
||||
)
|
||||
this.viewHelper.center = this.controls.target
|
||||
|
||||
this.handleResize()
|
||||
}
|
||||
|
||||
@@ -501,8 +643,16 @@ class Load3d {
|
||||
startAnimation() {
|
||||
const animate = () => {
|
||||
this.animationFrameId = requestAnimationFrame(animate)
|
||||
const delta = this.clock.getDelta()
|
||||
|
||||
if (this.viewHelper.animating) {
|
||||
this.viewHelper.update(delta)
|
||||
}
|
||||
|
||||
this.renderer.clear()
|
||||
this.controls.update()
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
this.viewHelper.render(this.renderer)
|
||||
}
|
||||
animate()
|
||||
}
|
||||
@@ -588,6 +738,7 @@ class Load3d {
|
||||
}
|
||||
|
||||
this.controls.dispose()
|
||||
this.viewHelper.dispose()
|
||||
this.renderer.dispose()
|
||||
this.renderer.domElement.remove()
|
||||
this.scene.clear()
|
||||
@@ -818,10 +969,12 @@ class Load3d {
|
||||
this.orthographicCamera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
this.renderer.clear()
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
const sceneData = this.renderer.domElement.toDataURL('image/png')
|
||||
|
||||
this.renderer.setClearColor(0x000000, 0)
|
||||
this.renderer.clear()
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
const maskData = this.renderer.domElement.toDataURL('image/png')
|
||||
|
||||
@@ -846,44 +999,6 @@ class Load3d {
|
||||
})
|
||||
}
|
||||
|
||||
setViewPosition(position: 'front' | 'top' | 'right' | 'isometric') {
|
||||
if (!this.currentModel) {
|
||||
return
|
||||
}
|
||||
|
||||
const box = new THREE.Box3()
|
||||
let center = new THREE.Vector3()
|
||||
let size = new THREE.Vector3()
|
||||
|
||||
if (this.currentModel) {
|
||||
box.setFromObject(this.currentModel)
|
||||
box.getCenter(center)
|
||||
box.getSize(size)
|
||||
}
|
||||
|
||||
const maxDim = Math.max(size.x, size.y, size.z)
|
||||
const distance = maxDim * 2
|
||||
|
||||
switch (position) {
|
||||
case 'front':
|
||||
this.activeCamera.position.set(0, 0, distance)
|
||||
break
|
||||
case 'top':
|
||||
this.activeCamera.position.set(0, distance, 0)
|
||||
break
|
||||
case 'right':
|
||||
this.activeCamera.position.set(distance, 0, 0)
|
||||
break
|
||||
case 'isometric':
|
||||
this.activeCamera.position.set(distance, distance, distance)
|
||||
break
|
||||
}
|
||||
|
||||
this.activeCamera.lookAt(center)
|
||||
this.controls.target.copy(center)
|
||||
this.controls.update()
|
||||
}
|
||||
|
||||
setBackgroundColor(color: string) {
|
||||
this.renderer.setClearColor(new THREE.Color(color))
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
@@ -1020,16 +1135,28 @@ class Load3dAnimation extends Load3d {
|
||||
})
|
||||
}
|
||||
|
||||
animate = () => {
|
||||
requestAnimationFrame(this.animate)
|
||||
|
||||
if (this.currentAnimation && this.isAnimationPlaying) {
|
||||
startAnimation() {
|
||||
const animate = () => {
|
||||
this.animationFrameId = requestAnimationFrame(animate)
|
||||
const delta = this.clock.getDelta()
|
||||
this.currentAnimation.update(delta)
|
||||
}
|
||||
|
||||
this.controls.update()
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
if (this.currentAnimation && this.isAnimationPlaying) {
|
||||
this.currentAnimation.update(delta)
|
||||
}
|
||||
|
||||
this.controls.update()
|
||||
|
||||
this.renderer.clear()
|
||||
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
|
||||
if (this.viewHelper.animating) {
|
||||
this.viewHelper.update(delta)
|
||||
}
|
||||
|
||||
this.viewHelper.render(this.renderer)
|
||||
}
|
||||
animate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1076,9 +1203,6 @@ function configureLoad3D(
|
||||
load3d: Load3d,
|
||||
loadFolder: 'input' | 'output',
|
||||
modelWidget: IWidget,
|
||||
showGrid: IWidget,
|
||||
cameraType: IWidget,
|
||||
view: IWidget,
|
||||
material: IWidget,
|
||||
bgColor: IWidget,
|
||||
lightIntensity: IWidget,
|
||||
@@ -1138,22 +1262,6 @@ function configureLoad3D(
|
||||
|
||||
modelWidget.callback = onModelWidgetUpdate
|
||||
|
||||
load3d.toggleGrid(showGrid.value as boolean)
|
||||
|
||||
showGrid.callback = (value: boolean) => {
|
||||
load3d.toggleGrid(value)
|
||||
}
|
||||
|
||||
load3d.toggleCamera(cameraType.value as 'perspective' | 'orthographic')
|
||||
|
||||
cameraType.callback = (value: 'perspective' | 'orthographic') => {
|
||||
load3d.toggleCamera(value)
|
||||
}
|
||||
|
||||
view.callback = (value: 'front' | 'top' | 'right' | 'isometric') => {
|
||||
load3d.setViewPosition(value)
|
||||
}
|
||||
|
||||
material.callback = (value: 'original' | 'normal' | 'wireframe') => {
|
||||
load3d.setMaterialMode(value)
|
||||
}
|
||||
@@ -1312,14 +1420,6 @@ app.registerExtension({
|
||||
(w: IWidget) => w.name === 'model_file'
|
||||
)
|
||||
|
||||
const showGrid = node.widgets.find((w: IWidget) => w.name === 'show_grid')
|
||||
|
||||
const cameraType = node.widgets.find(
|
||||
(w: IWidget) => w.name === 'camera_type'
|
||||
)
|
||||
|
||||
const view = node.widgets.find((w: IWidget) => w.name === 'view')
|
||||
|
||||
const material = node.widgets.find((w: IWidget) => w.name === 'material')
|
||||
|
||||
const bgColor = node.widgets.find((w: IWidget) => w.name === 'bg_color')
|
||||
@@ -1353,9 +1453,6 @@ app.registerExtension({
|
||||
load3d,
|
||||
'input',
|
||||
modelWidget,
|
||||
showGrid,
|
||||
cameraType,
|
||||
view,
|
||||
material,
|
||||
bgColor,
|
||||
lightIntensity,
|
||||
@@ -1569,14 +1666,6 @@ app.registerExtension({
|
||||
(w: IWidget) => w.name === 'model_file'
|
||||
)
|
||||
|
||||
const showGrid = node.widgets.find((w: IWidget) => w.name === 'show_grid')
|
||||
|
||||
const cameraType = node.widgets.find(
|
||||
(w: IWidget) => w.name === 'camera_type'
|
||||
)
|
||||
|
||||
const view = node.widgets.find((w: IWidget) => w.name === 'view')
|
||||
|
||||
const material = node.widgets.find((w: IWidget) => w.name === 'material')
|
||||
|
||||
const bgColor = node.widgets.find((w: IWidget) => w.name === 'bg_color')
|
||||
@@ -1621,9 +1710,6 @@ app.registerExtension({
|
||||
load3d,
|
||||
'input',
|
||||
modelWidget,
|
||||
showGrid,
|
||||
cameraType,
|
||||
view,
|
||||
material,
|
||||
bgColor,
|
||||
lightIntensity,
|
||||
@@ -1652,6 +1738,8 @@ app.registerExtension({
|
||||
sceneWidget.serializeValue = async () => {
|
||||
node.properties['Camera Info'] = JSON.stringify(load3d.getCameraState())
|
||||
|
||||
load3d.toggleAnimation(false)
|
||||
|
||||
const { scene: imageData, mask: maskData } = await load3d.captureScene(
|
||||
w.value,
|
||||
h.value
|
||||
@@ -1758,14 +1846,6 @@ app.registerExtension({
|
||||
(w: IWidget) => w.name === 'model_file'
|
||||
)
|
||||
|
||||
const showGrid = node.widgets.find((w: IWidget) => w.name === 'show_grid')
|
||||
|
||||
const cameraType = node.widgets.find(
|
||||
(w: IWidget) => w.name === 'camera_type'
|
||||
)
|
||||
|
||||
const view = node.widgets.find((w: IWidget) => w.name === 'view')
|
||||
|
||||
const material = node.widgets.find((w: IWidget) => w.name === 'material')
|
||||
|
||||
const bgColor = node.widgets.find((w: IWidget) => w.name === 'bg_color')
|
||||
@@ -1801,9 +1881,6 @@ app.registerExtension({
|
||||
load3d,
|
||||
'output',
|
||||
modelWidget,
|
||||
showGrid,
|
||||
cameraType,
|
||||
view,
|
||||
material,
|
||||
bgColor,
|
||||
lightIntensity,
|
||||
|
||||
@@ -1125,7 +1125,7 @@ class MaskEditorDialog extends ComfyDialog {
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
console.log('Failed to upload mask:', response)
|
||||
this.uploadMask(filepath, formData, 2)
|
||||
this.uploadMask(filepath, formData, retries - 1)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -68,7 +68,19 @@
|
||||
"disableAll": "Disable All",
|
||||
"command": "Command",
|
||||
"keybinding": "Keybinding",
|
||||
"upload": "Upload"
|
||||
"upload": "Upload",
|
||||
"export": "Export",
|
||||
"workflow": "Workflow"
|
||||
},
|
||||
"issueReport": {
|
||||
"submitErrorReport": "Submit Error Report (Optional)",
|
||||
"provideEmail": "Give us your email (Optional)",
|
||||
"provideAdditionalDetails": "Provide additional details (optional)",
|
||||
"stackTrace": "Stack Trace",
|
||||
"systemStats": "System Stats",
|
||||
"contactFollowUp": "Contact me for follow up",
|
||||
"notifyResolve": "Notify me when resolved",
|
||||
"helpFix": "Help Fix This"
|
||||
},
|
||||
"color": {
|
||||
"default": "Default",
|
||||
@@ -169,21 +181,26 @@
|
||||
},
|
||||
"settings": {
|
||||
"autoUpdate": "Automatic Updates",
|
||||
"allowMetrics": "Crash Reports",
|
||||
"autoUpdateDescription": "Automatically download and install updates when they become available. You'll always be notified before updates are installed.",
|
||||
"allowMetricsDescription": "Help improve ComfyUI by sending anonymous crash reports. No personal information or workflow content will be collected. This can be disabled at any time in the settings menu.",
|
||||
"allowMetrics": "Usage Metrics",
|
||||
"autoUpdateDescription": "Automatically download updates when they become available. You will be notified before updates are installed.",
|
||||
"allowMetricsDescription": "Help improve ComfyUI by sending anonymous usage metrics. No personal information or workflow content will be collected.",
|
||||
"learnMoreAboutData": "Learn more about data collection",
|
||||
"dataCollectionDialog": {
|
||||
"title": "About Data Collection",
|
||||
"whatWeCollect": "What we collect:",
|
||||
"whatWeDoNotCollect": "What we don't collect:",
|
||||
"errorReports": "Error message and stack trace",
|
||||
"systemInfo": "Hardware, OS type, and app version",
|
||||
"personalInformation": "Personal information",
|
||||
"workflowContent": "Workflow content",
|
||||
"fileSystemInformation": "File system information",
|
||||
"workflowContents": "Workflow contents",
|
||||
"customNodeConfigurations": "Custom node configurations"
|
||||
"collect": {
|
||||
"errorReports": "Error message and stack trace",
|
||||
"systemInfo": "Hardware, OS type, and app version",
|
||||
"userJourneyEvents": "User journey events",
|
||||
"userJourneyTooltip": "User journey events are used to track the user's journey through the app installation process. The event collection ends on the first successful ComfyUI workflow run."
|
||||
},
|
||||
"doNotCollect": {
|
||||
"personalInformation": "Personal information",
|
||||
"fileSystemInformation": "File system information",
|
||||
"workflowContents": "Workflow contents",
|
||||
"customNodeConfigurations": "Custom node configurations"
|
||||
}
|
||||
}
|
||||
},
|
||||
"customNodes": "Custom Nodes",
|
||||
@@ -217,7 +234,8 @@
|
||||
"openWorkflow": "Open workflow in local file system",
|
||||
"newBlankWorkflow": "Create a new blank workflow",
|
||||
"nodeLibraryTab": {
|
||||
"sortOrder": "Sort Order"
|
||||
"sortOrder": "Sort Order",
|
||||
"groupingType": "Grouping Type"
|
||||
},
|
||||
"modelLibrary": "Model Library",
|
||||
"downloads": "Downloads",
|
||||
@@ -278,7 +296,9 @@
|
||||
"closeTab": "Close Tab",
|
||||
"closeTabsToLeft": "Close Tabs to Left",
|
||||
"closeTabsToRight": "Close Tabs to Right",
|
||||
"closeOtherTabs": "Close Other Tabs"
|
||||
"closeOtherTabs": "Close Other Tabs",
|
||||
"addToBookmarks": "Add to Bookmarks",
|
||||
"removeFromBookmarks": "Remove from Bookmarks"
|
||||
},
|
||||
"templateWorkflows": {
|
||||
"title": "Get Started with a Template",
|
||||
|
||||
@@ -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"
|
||||
@@ -293,7 +301,8 @@
|
||||
"name": "Opened workflows position",
|
||||
"options": {
|
||||
"Sidebar": "Sidebar",
|
||||
"Topbar": "Topbar"
|
||||
"Topbar": "Topbar",
|
||||
"Topbar (2nd-row)": "Topbar (2nd-row)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"enableAll": "Activer tout",
|
||||
"error": "Erreur",
|
||||
"experimental": "BETA",
|
||||
"export": "Exportation",
|
||||
"extensionName": "Nom de l'extension",
|
||||
"findIssues": "Trouver des problèmes",
|
||||
"firstTimeUIMessage": "C'est la première fois que vous utilisez la nouvelle interface utilisateur. Choisissez \"Menu > Utiliser le nouveau menu > Désactivé\" pour restaurer l'ancienne interface utilisateur.",
|
||||
@@ -131,7 +132,8 @@
|
||||
"systemInfo": "Informations système",
|
||||
"terminal": "Terminal",
|
||||
"upload": "Téléverser",
|
||||
"videoFailedToLoad": "Échec du chargement de la vidéo"
|
||||
"videoFailedToLoad": "Échec du chargement de la vidéo",
|
||||
"workflow": "Flux de travail"
|
||||
},
|
||||
"graphCanvasMenu": {
|
||||
"fitView": "Adapter la vue",
|
||||
@@ -204,27 +206,42 @@
|
||||
"pathValidationFailed": "Échec de la validation du chemin",
|
||||
"selectItemsToMigrate": "Sélectionnez les éléments à migrer",
|
||||
"settings": {
|
||||
"allowMetrics": "Rapports de plantage",
|
||||
"allowMetricsDescription": "Aidez à améliorer ComfyUI en envoyant des rapports de plantage anonymes. Aucune information personnelle ou contenu de flux de travail ne sera collecté. Cela peut être désactivé à tout moment dans le menu des paramètres.",
|
||||
"allowMetrics": "Métriques d'utilisation",
|
||||
"allowMetricsDescription": "Aidez à améliorer ComfyUI en envoyant des métriques d'utilisation anonymes. Aucune information personnelle ou contenu de flux de travail ne sera collecté.",
|
||||
"autoUpdate": "Mises à jour automatiques",
|
||||
"autoUpdateDescription": "Téléchargez et installez automatiquement les mises à jour lorsqu'elles deviennent disponibles. Vous serez toujours informé avant l'installation des mises à jour.",
|
||||
"dataCollectionDialog": {
|
||||
"customNodeConfigurations": "Configurations de nœuds personnalisés",
|
||||
"errorReports": "Message d'erreur et trace de la pile",
|
||||
"fileSystemInformation": "Informations sur le système de fichiers",
|
||||
"personalInformation": "Informations personnelles",
|
||||
"systemInfo": "Matériel, type d'OS et version de l'application",
|
||||
"collect": {
|
||||
"errorReports": "Message d'erreur et trace de la pile",
|
||||
"systemInfo": "Matériel, type de système d'exploitation et version de l'application",
|
||||
"userJourneyEvents": "Événements du parcours utilisateur",
|
||||
"userJourneyTooltip": "Les événements du parcours utilisateur sont utilisés pour suivre le parcours de l'utilisateur lors du processus d'installation de l'application. La collecte d'événements se termine lors de la première exécution réussie du flux de travail ComfyUI."
|
||||
},
|
||||
"doNotCollect": {
|
||||
"customNodeConfigurations": "Configurations de nœud personnalisées",
|
||||
"fileSystemInformation": "Informations sur le système de fichiers",
|
||||
"personalInformation": "Informations personnelles",
|
||||
"workflowContents": "Contenus du flux de travail"
|
||||
},
|
||||
"title": "À propos de la collecte de données",
|
||||
"whatWeCollect": "Ce que nous collectons :",
|
||||
"whatWeDoNotCollect": "Ce que nous ne collectons pas :",
|
||||
"workflowContent": "Contenu du flux de travail",
|
||||
"workflowContents": "Contenus du flux de travail"
|
||||
"whatWeDoNotCollect": "Ce que nous ne collectons pas :"
|
||||
},
|
||||
"learnMoreAboutData": "En savoir plus sur la collecte de données"
|
||||
},
|
||||
"systemLocations": "Emplacements système",
|
||||
"unhandledError": "Erreur inconnue"
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "Contactez-moi pour un suivi",
|
||||
"helpFix": "Aidez à résoudre cela",
|
||||
"notifyResolve": "Prévenez-moi lorsque résolu",
|
||||
"provideAdditionalDetails": "Fournir des détails supplémentaires (facultatif)",
|
||||
"provideEmail": "Donnez-nous votre email (Facultatif)",
|
||||
"stackTrace": "Trace de la pile",
|
||||
"submitErrorReport": "Soumettre un rapport d'erreur (Facultatif)",
|
||||
"systemStats": "Statistiques du système"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "File d'attente automatique",
|
||||
"batchCount": "Nombre de lots",
|
||||
@@ -622,11 +639,13 @@
|
||||
"workflows": "Flux de travail"
|
||||
},
|
||||
"tabMenu": {
|
||||
"addToBookmarks": "Ajouter aux Favoris",
|
||||
"closeOtherTabs": "Fermer les autres onglets",
|
||||
"closeTab": "Fermer l'onglet",
|
||||
"closeTabsToLeft": "Fermer les onglets à gauche",
|
||||
"closeTabsToRight": "Fermer les onglets à droite",
|
||||
"duplicateTab": "Dupliquer l'onglet"
|
||||
"duplicateTab": "Dupliquer l'onglet",
|
||||
"removeFromBookmarks": "Retirer des Favoris"
|
||||
},
|
||||
"templateWorkflows": {
|
||||
"template": {
|
||||
|
||||
@@ -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"
|
||||
@@ -293,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": {
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"enableAll": "すべて有効にする",
|
||||
"error": "エラー",
|
||||
"experimental": "ベータ",
|
||||
"export": "エクスポート",
|
||||
"extensionName": "拡張機能名",
|
||||
"findIssues": "問題を見つける",
|
||||
"firstTimeUIMessage": "新しいUIを初めて使用しています。「メニュー > 新しいメニューを使用 > 無効」を選択して古いUIに戻してください。",
|
||||
@@ -131,7 +132,8 @@
|
||||
"systemInfo": "システム情報",
|
||||
"terminal": "ターミナル",
|
||||
"upload": "アップロード",
|
||||
"videoFailedToLoad": "ビデオの読み込みに失敗しました"
|
||||
"videoFailedToLoad": "ビデオの読み込みに失敗しました",
|
||||
"workflow": "ワークフロー"
|
||||
},
|
||||
"graphCanvasMenu": {
|
||||
"fitView": "ビューに合わせる",
|
||||
@@ -204,27 +206,42 @@
|
||||
"pathValidationFailed": "パスの検証に失敗しました",
|
||||
"selectItemsToMigrate": "移行する項目を選択",
|
||||
"settings": {
|
||||
"allowMetrics": "クラッシュレポート",
|
||||
"allowMetricsDescription": "ComfyUIの改善に協力してください。匿名のクラッシュレポートを送信します。個人情報やワークフロー内容は収集されません。この設定はいつでも無効にできます。",
|
||||
"allowMetrics": "使用状況のメトリクス",
|
||||
"allowMetricsDescription": "匿名の使用状況メトリクスを送信してComfyUIを改善します。個人情報やワークフローの内容は収集されません。",
|
||||
"autoUpdate": "自動更新",
|
||||
"autoUpdateDescription": "更新が利用可能になると、自動的にダウンロードおよびインストールを行います。インストール前に通知が表示されます。",
|
||||
"dataCollectionDialog": {
|
||||
"customNodeConfigurations": "カスタムノード設定",
|
||||
"errorReports": "エラーメッセージとスタックトレース",
|
||||
"fileSystemInformation": "ファイルシステム情報",
|
||||
"personalInformation": "個人情報",
|
||||
"systemInfo": "ハードウェア、OSの種類、アプリのバージョン",
|
||||
"collect": {
|
||||
"errorReports": "エラーメッセージとスタックトレース",
|
||||
"systemInfo": "ハードウェア、OSタイプ、アプリバージョン",
|
||||
"userJourneyEvents": "ユーザージャーニーイベント",
|
||||
"userJourneyTooltip": "ユーザージャーニーイベントは、アプリのインストールプロセスを通じてユーザーの旅を追跡するために使用されます。イベントの収集は、最初の成功したComfyUIワークフローの実行で終了します。"
|
||||
},
|
||||
"doNotCollect": {
|
||||
"customNodeConfigurations": "カスタムノードの設定",
|
||||
"fileSystemInformation": "ファイルシステム情報",
|
||||
"personalInformation": "個人情報",
|
||||
"workflowContents": "ワークフローの内容"
|
||||
},
|
||||
"title": "データ収集について",
|
||||
"whatWeCollect": "収集内容:",
|
||||
"whatWeDoNotCollect": "収集しない内容:",
|
||||
"workflowContent": "ワークフロー内容",
|
||||
"workflowContents": "ワークフロー内容"
|
||||
"whatWeDoNotCollect": "収集しない内容:"
|
||||
},
|
||||
"learnMoreAboutData": "データ収集の詳細を見る"
|
||||
},
|
||||
"systemLocations": "システムの場所",
|
||||
"unhandledError": "未知のエラー"
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "フォローアップのために私に連絡する",
|
||||
"helpFix": "これを修正するのを助ける",
|
||||
"notifyResolve": "解決したときに通知する",
|
||||
"provideAdditionalDetails": "追加の詳細を提供する(オプション)",
|
||||
"provideEmail": "あなたのメールアドレスを教えてください(オプション)",
|
||||
"stackTrace": "スタックトレース",
|
||||
"submitErrorReport": "エラーレポートを提出する(オプション)",
|
||||
"systemStats": "システム統計"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "自動キュー",
|
||||
"batchCount": "バッチ数",
|
||||
@@ -622,11 +639,13 @@
|
||||
"workflows": "ワークフロー"
|
||||
},
|
||||
"tabMenu": {
|
||||
"addToBookmarks": "ブックマークに追加",
|
||||
"closeOtherTabs": "他のタブを閉じる",
|
||||
"closeTab": "タブを閉じる",
|
||||
"closeTabsToLeft": "左のタブを閉じる",
|
||||
"closeTabsToRight": "右のタブを閉じる",
|
||||
"duplicateTab": "タブを複製"
|
||||
"duplicateTab": "タブを複製",
|
||||
"removeFromBookmarks": "ブックマークから削除"
|
||||
},
|
||||
"templateWorkflows": {
|
||||
"template": {
|
||||
|
||||
@@ -3,7 +3,15 @@
|
||||
"name": "自動的に更新を確認する"
|
||||
},
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "匿名のクラッシュレポートを送信する"
|
||||
"name": "匿名の使用統計を送信する"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "ウィンドウスタイル",
|
||||
"options": {
|
||||
"custom": "カスタム",
|
||||
"default": "デフォルト"
|
||||
},
|
||||
"tooltip": "システムタイトルバーを非表示にするにはカスタムオプションを選択してください"
|
||||
},
|
||||
"Comfy_ConfirmClear": {
|
||||
"name": "ワークフローをクリアする際に確認を要求する"
|
||||
@@ -293,7 +301,8 @@
|
||||
"name": "開いているワークフローの位置",
|
||||
"options": {
|
||||
"Sidebar": "サイドバー",
|
||||
"Topbar": "トップバー"
|
||||
"Topbar": "トップバー",
|
||||
"Topbar (2nd-row)": "トップバー(2行目)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"enableAll": "모두 활성화",
|
||||
"error": "오류",
|
||||
"experimental": "베타",
|
||||
"export": "내보내기",
|
||||
"extensionName": "확장 이름",
|
||||
"findIssues": "문제 찾기",
|
||||
"firstTimeUIMessage": "새 UI를 처음 사용합니다. \"메뉴 > 새 메뉴 사용 > 비활성화\"를 선택하여 이전 UI로 복원하세요.",
|
||||
@@ -131,7 +132,8 @@
|
||||
"systemInfo": "시스템 정보",
|
||||
"terminal": "터미널",
|
||||
"upload": "업로드",
|
||||
"videoFailedToLoad": "비디오를 로드하지 못했습니다."
|
||||
"videoFailedToLoad": "비디오를 로드하지 못했습니다.",
|
||||
"workflow": "워크플로우"
|
||||
},
|
||||
"graphCanvasMenu": {
|
||||
"fitView": "보기 맞춤",
|
||||
@@ -204,27 +206,42 @@
|
||||
"pathValidationFailed": "경로 유효성 검사 실패",
|
||||
"selectItemsToMigrate": "마이그레이션 항목 선택",
|
||||
"settings": {
|
||||
"allowMetrics": "충돌 보고서",
|
||||
"allowMetricsDescription": "익명의 충돌 보고서를 보내 ComfyUI 개선에 도움을 줍니다. 개인 정보나 워크플로 내용은 수집되지 않습니다. 이는 설정 메뉴에서 언제든지 비활성화할 수 있습니다.",
|
||||
"allowMetrics": "사용 통계",
|
||||
"allowMetricsDescription": "익명의 사용 통계를 보내 ComfyUI를 개선하는 데 도움을 줍니다. 개인 정보나 워크플로우 내용은 수집되지 않습니다.",
|
||||
"autoUpdate": "자동 업데이트",
|
||||
"autoUpdateDescription": "업데이트가 가능해지면 자동으로 다운로드하고 설치합니다. 업데이트가 설치되기 전에 항상 알림을 받습니다.",
|
||||
"dataCollectionDialog": {
|
||||
"customNodeConfigurations": "사용자 정의 노드 설정",
|
||||
"errorReports": "오류 메시지 및 스택 추적",
|
||||
"fileSystemInformation": "파일 시스템 정보",
|
||||
"personalInformation": "개인 정보",
|
||||
"systemInfo": "하드웨어, OS 유형 및 앱 버전",
|
||||
"collect": {
|
||||
"errorReports": "오류 메시지 및 스택 추적",
|
||||
"systemInfo": "하드웨어, OS 유형, 앱 버전",
|
||||
"userJourneyEvents": "사용자 여정 이벤트",
|
||||
"userJourneyTooltip": "사용자 여정 이벤트는 앱 설치 과정을 통한 사용자의 여정을 추적하는 데 사용됩니다. 이벤트 수집은 첫 번째 성공적인 ComfyUI 워크플로우 실행에서 종료됩니다."
|
||||
},
|
||||
"doNotCollect": {
|
||||
"customNodeConfigurations": "사용자 정의 노드 구성",
|
||||
"fileSystemInformation": "파일 시스템 정보",
|
||||
"personalInformation": "개인 정보",
|
||||
"workflowContents": "워크플로우 내용"
|
||||
},
|
||||
"title": "데이터 수집 안내",
|
||||
"whatWeCollect": "수집하는 정보:",
|
||||
"whatWeDoNotCollect": "수집하지 않는 정보:",
|
||||
"workflowContent": "워크플로 내용",
|
||||
"workflowContents": "워크플로 내용"
|
||||
"whatWeDoNotCollect": "수집하지 않는 정보:"
|
||||
},
|
||||
"learnMoreAboutData": "데이터 수집에 대해 더 알아보기"
|
||||
},
|
||||
"systemLocations": "시스템 위치",
|
||||
"unhandledError": "알 수 없는 오류"
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "추적 조사를 위해 연락해 주세요",
|
||||
"helpFix": "이 문제 해결에 도움을 주세요",
|
||||
"notifyResolve": "해결되었을 때 알려주세요",
|
||||
"provideAdditionalDetails": "추가 세부 사항 제공 (선택 사항)",
|
||||
"provideEmail": "이메일을 알려주세요 (선택 사항)",
|
||||
"stackTrace": "스택 추적",
|
||||
"submitErrorReport": "오류 보고서 제출 (선택 사항)",
|
||||
"systemStats": "시스템 통계"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "자동 실행 큐",
|
||||
"batchCount": "배치 수",
|
||||
@@ -622,11 +639,13 @@
|
||||
"workflows": "워크플로"
|
||||
},
|
||||
"tabMenu": {
|
||||
"addToBookmarks": "북마크에 추가",
|
||||
"closeOtherTabs": "다른 탭 닫기",
|
||||
"closeTab": "탭 닫기",
|
||||
"closeTabsToLeft": "왼쪽 탭 닫기",
|
||||
"closeTabsToRight": "오른쪽 탭 닫기",
|
||||
"duplicateTab": "탭 복제"
|
||||
"duplicateTab": "탭 복제",
|
||||
"removeFromBookmarks": "북마크에서 제거"
|
||||
},
|
||||
"templateWorkflows": {
|
||||
"template": {
|
||||
|
||||
@@ -3,7 +3,15 @@
|
||||
"name": "자동 업데이트 확인"
|
||||
},
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "익명으로 충돌 보고서 전송"
|
||||
"name": "익명 사용 통계 보내기"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "창 스타일",
|
||||
"options": {
|
||||
"custom": "사용자 정의",
|
||||
"default": "기본"
|
||||
},
|
||||
"tooltip": "시스템 제목 표시 줄을 숨기려면 사용자 정의 옵션을 선택하세요"
|
||||
},
|
||||
"Comfy_ConfirmClear": {
|
||||
"name": "워크플로 비우기 시 확인 요구"
|
||||
@@ -293,7 +301,8 @@
|
||||
"name": "열린 워크플로 위치",
|
||||
"options": {
|
||||
"Sidebar": "사이드바",
|
||||
"Topbar": "상단바"
|
||||
"Topbar": "상단바",
|
||||
"Topbar (2nd-row)": "상단바 (2번째 행)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"enableAll": "Включить все",
|
||||
"error": "Ошибка",
|
||||
"experimental": "БЕТА",
|
||||
"export": "Экспорт",
|
||||
"extensionName": "Имя расширения",
|
||||
"findIssues": "Найти проблемы",
|
||||
"firstTimeUIMessage": "Вы впервые используете новый интерфейс. Выберите \"Меню > Использовать новое меню > Отключено\", чтобы восстановить старый интерфейс.",
|
||||
@@ -131,7 +132,8 @@
|
||||
"systemInfo": "Информация о системе",
|
||||
"terminal": "Терминал",
|
||||
"upload": "Загрузить",
|
||||
"videoFailedToLoad": "Не удалось загрузить видео"
|
||||
"videoFailedToLoad": "Не удалось загрузить видео",
|
||||
"workflow": "Рабочий процесс"
|
||||
},
|
||||
"graphCanvasMenu": {
|
||||
"fitView": "Подгонять под выделенные",
|
||||
@@ -204,27 +206,42 @@
|
||||
"pathValidationFailed": "Не удалось проверить путь",
|
||||
"selectItemsToMigrate": "Выберите элементы для миграции",
|
||||
"settings": {
|
||||
"allowMetrics": "Отчеты о сбоях",
|
||||
"allowMetricsDescription": "Помогите улучшить ComfyUI, отправляя анонимные отчеты о сбоях. Личная информация или содержимое рабочего процесса не будут собираться. Это можно отключить в любое время в меню настроек.",
|
||||
"allowMetrics": "Метрики использования",
|
||||
"allowMetricsDescription": "Помогите улучшить ComfyUI, отправляя анонимные метрики использования. Личная информация или содержание рабочего процесса не будут собираться.",
|
||||
"autoUpdate": "Автоматические обновления",
|
||||
"autoUpdateDescription": "Автоматически загружать и устанавливать обновления, когда они становятся доступными. Вы всегда будете уведомлены перед установкой обновлений.",
|
||||
"dataCollectionDialog": {
|
||||
"customNodeConfigurations": "Конфигурации пользовательских узлов",
|
||||
"errorReports": "Сообщения об ошибках и трассировка стека",
|
||||
"fileSystemInformation": "Информация о файловой системе",
|
||||
"personalInformation": "Личная информация",
|
||||
"systemInfo": "Аппаратное обеспечение, тип ОС и версия приложения",
|
||||
"collect": {
|
||||
"errorReports": "Сообщение об ошибке и трассировка стека",
|
||||
"systemInfo": "Аппаратное обеспечение, тип ОС и версия приложения",
|
||||
"userJourneyEvents": "События пользовательского пути",
|
||||
"userJourneyTooltip": "События пользовательского пути используются для отслеживания пути пользователя в процессе установки приложения. Сбор событий заканчивается после первого успешного запуска рабочего процесса ComfyUI."
|
||||
},
|
||||
"doNotCollect": {
|
||||
"customNodeConfigurations": "Пользовательские конфигурации узлов",
|
||||
"fileSystemInformation": "Информация о файловой системе",
|
||||
"personalInformation": "Личная информация",
|
||||
"workflowContents": "Содержание рабочего процесса"
|
||||
},
|
||||
"title": "О сборе данных",
|
||||
"whatWeCollect": "Что мы собираем:",
|
||||
"whatWeDoNotCollect": "Что мы не собираем:",
|
||||
"workflowContent": "Содержимое рабочего процесса",
|
||||
"workflowContents": "Содержимое рабочего процесса"
|
||||
"whatWeDoNotCollect": "Что мы не собираем:"
|
||||
},
|
||||
"learnMoreAboutData": "Узнать больше о сборе данных"
|
||||
},
|
||||
"systemLocations": "Системные места",
|
||||
"unhandledError": "Неизвестная ошибка"
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "Свяжитесь со мной для уточнения",
|
||||
"helpFix": "Помочь исправить это",
|
||||
"notifyResolve": "Уведомить меня, когда проблема будет решена",
|
||||
"provideAdditionalDetails": "Предоставьте дополнительные сведения (необязательно)",
|
||||
"provideEmail": "Укажите вашу электронную почту (необязательно)",
|
||||
"stackTrace": "Трассировка стека",
|
||||
"submitErrorReport": "Отправить отчет об ошибке (необязательно)",
|
||||
"systemStats": "Статистика системы"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "Автоочередь",
|
||||
"batchCount": "Количество пакетов",
|
||||
@@ -622,11 +639,13 @@
|
||||
"workflows": "Рабочие процессы"
|
||||
},
|
||||
"tabMenu": {
|
||||
"addToBookmarks": "Добавить в закладки",
|
||||
"closeOtherTabs": "Закрыть другие вкладки",
|
||||
"closeTab": "Закрыть вкладку",
|
||||
"closeTabsToLeft": "Закрыть вкладки слева",
|
||||
"closeTabsToRight": "Закрыть вкладки справа",
|
||||
"duplicateTab": "Дублировать вкладку"
|
||||
"duplicateTab": "Дублировать вкладку",
|
||||
"removeFromBookmarks": "Удалить из закладок"
|
||||
},
|
||||
"templateWorkflows": {
|
||||
"template": {
|
||||
|
||||
@@ -3,7 +3,15 @@
|
||||
"name": "Автоматически проверять обновления"
|
||||
},
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "Отправлять анонимные отчеты о сбоях"
|
||||
"name": "Отправлять анонимную статистику использования"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "Стиль окна",
|
||||
"options": {
|
||||
"custom": "пользовательский",
|
||||
"default": "по умолчанию"
|
||||
},
|
||||
"tooltip": "Выберите пользовательский вариант, чтобы скрыть системную строку заголовка"
|
||||
},
|
||||
"Comfy_ConfirmClear": {
|
||||
"name": "Требовать подтверждение при очистке рабочего процесса"
|
||||
@@ -293,7 +301,8 @@
|
||||
"name": "Положение открытых рабочих процессов",
|
||||
"options": {
|
||||
"Sidebar": "Боковая панель",
|
||||
"Topbar": "Верхняя панель"
|
||||
"Topbar": "Верхняя панель",
|
||||
"Topbar (2nd-row)": "Топбар (2-й ряд)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"enableAll": "启用全部",
|
||||
"error": "错误",
|
||||
"experimental": "测试版",
|
||||
"export": "导出",
|
||||
"extensionName": "扩展名称",
|
||||
"findIssues": "查找问题",
|
||||
"firstTimeUIMessage": "这是您第一次使用新界面。选择 \"菜单 > 使用新菜单 > 禁用\" 来恢复旧界面。",
|
||||
@@ -131,7 +132,8 @@
|
||||
"systemInfo": "系统信息",
|
||||
"terminal": "终端",
|
||||
"upload": "上传",
|
||||
"videoFailedToLoad": "视频加载失败"
|
||||
"videoFailedToLoad": "视频加载失败",
|
||||
"workflow": "工作流"
|
||||
},
|
||||
"graphCanvasMenu": {
|
||||
"fitView": "适应视图",
|
||||
@@ -204,27 +206,42 @@
|
||||
"pathValidationFailed": "路径验证失败",
|
||||
"selectItemsToMigrate": "选择要迁移的项目",
|
||||
"settings": {
|
||||
"allowMetrics": "崩溃报告",
|
||||
"allowMetricsDescription": "发送匿名崩溃报告帮助改善 ComfyUI。报告不会收集任何个人信息或工作流内容。您可以随时在设置菜单中禁用此功能。",
|
||||
"allowMetrics": "使用情况指标",
|
||||
"allowMetricsDescription": "通过发送匿名使用情况指标来帮助改进ComfyUI。不会收集任何个人信息或工作流内容。",
|
||||
"autoUpdate": "自动更新",
|
||||
"autoUpdateDescription": "更新可用时自动更新。您将在安装更新之前收到通知。",
|
||||
"dataCollectionDialog": {
|
||||
"customNodeConfigurations": "自定义节点配置",
|
||||
"errorReports": "错误信息和堆栈跟踪",
|
||||
"fileSystemInformation": "文件系统信息",
|
||||
"personalInformation": "个人信息",
|
||||
"systemInfo": "硬件、操作系统类型和应用版本",
|
||||
"collect": {
|
||||
"errorReports": "错误报告和堆栈跟踪",
|
||||
"systemInfo": "硬件,操作系统类型和应用版本",
|
||||
"userJourneyEvents": "用户旅程事件",
|
||||
"userJourneyTooltip": "用户旅程事件用于跟踪用户通过应用安装过程的旅程。事件收集在第一次成功运行ComfyUI工作流后结束。"
|
||||
},
|
||||
"doNotCollect": {
|
||||
"customNodeConfigurations": "自定义节点配置",
|
||||
"fileSystemInformation": "文件系统信息",
|
||||
"personalInformation": "个人信息",
|
||||
"workflowContents": "工作流内容"
|
||||
},
|
||||
"title": "关于数据收集",
|
||||
"whatWeCollect": "我们收集的内容:",
|
||||
"whatWeDoNotCollect": "我们不收集的内容:",
|
||||
"workflowContent": "工作流内容",
|
||||
"workflowContents": "工作流内容"
|
||||
"whatWeDoNotCollect": "我们不收集的内容:"
|
||||
},
|
||||
"learnMoreAboutData": "了解更多关于数据收集的信息"
|
||||
},
|
||||
"systemLocations": "系统位置",
|
||||
"unhandledError": "未知错误"
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "跟进联系我",
|
||||
"helpFix": "帮助修复这个",
|
||||
"notifyResolve": "解决时通知我",
|
||||
"provideAdditionalDetails": "提供额外的详细信息(可选)",
|
||||
"provideEmail": "提供您的电子邮件(可选)",
|
||||
"stackTrace": "堆栈跟踪",
|
||||
"submitErrorReport": "提交错误报告(可选)",
|
||||
"systemStats": "系统状态"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "自动执行",
|
||||
"batchCount": "批次数量",
|
||||
@@ -622,11 +639,13 @@
|
||||
"workflows": "工作流"
|
||||
},
|
||||
"tabMenu": {
|
||||
"addToBookmarks": "添加到书签",
|
||||
"closeOtherTabs": "关闭其他标签",
|
||||
"closeTab": "关闭标签",
|
||||
"closeTabsToLeft": "关闭左侧标签",
|
||||
"closeTabsToRight": "关闭右侧标签",
|
||||
"duplicateTab": "复制标签"
|
||||
"duplicateTab": "复制标签",
|
||||
"removeFromBookmarks": "从书签中移除"
|
||||
},
|
||||
"templateWorkflows": {
|
||||
"template": {
|
||||
|
||||
@@ -3,7 +3,15 @@
|
||||
"name": "自动检查更新"
|
||||
},
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "发送匿名崩溃报告"
|
||||
"name": "发送匿名使用情况统计"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "窗口样式",
|
||||
"options": {
|
||||
"custom": "自定义",
|
||||
"default": "默认"
|
||||
},
|
||||
"tooltip": "选择自定义选项以隐藏系统标题栏"
|
||||
},
|
||||
"Comfy_ConfirmClear": {
|
||||
"name": "清除工作流时需要确认"
|
||||
@@ -293,7 +301,8 @@
|
||||
"name": "已打开工作流的位置",
|
||||
"options": {
|
||||
"Sidebar": "侧边栏",
|
||||
"Topbar": "顶部栏"
|
||||
"Topbar": "顶部栏",
|
||||
"Topbar (2nd-row)": "顶部栏 (第二行)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
|
||||
12
src/main.ts
12
src/main.ts
@@ -2,6 +2,7 @@
|
||||
import '@comfyorg/litegraph/style.css'
|
||||
import { definePreset } from '@primevue/themes'
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import * as Sentry from '@sentry/vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import 'primeicons/primeicons.css'
|
||||
import PrimeVue from 'primevue/config'
|
||||
@@ -24,6 +25,17 @@ const ComfyUIPreset = definePreset(Aura, {
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
Sentry.init({
|
||||
app,
|
||||
dsn: __SENTRY_DSN__,
|
||||
enabled: __SENTRY_ENABLED__,
|
||||
release: __COMFYUI_FRONTEND_VERSION__,
|
||||
integrations: [],
|
||||
autoSessionTracking: false,
|
||||
defaultIntegrations: false,
|
||||
normalizeDepth: 8,
|
||||
tracesSampleRate: 0
|
||||
})
|
||||
app.directive('tooltip', Tooltip)
|
||||
app
|
||||
.use(router)
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
ExecutingWsMessage,
|
||||
ExecutionCachedWsMessage,
|
||||
ExecutionErrorWsMessage,
|
||||
ExecutionInterruptedWsMessage,
|
||||
ExecutionStartWsMessage,
|
||||
ExecutionSuccessWsMessage,
|
||||
ExtensionsResponse,
|
||||
@@ -59,6 +60,7 @@ interface BackendApiCalls {
|
||||
execution_start: ExecutionStartWsMessage
|
||||
execution_success: ExecutionSuccessWsMessage
|
||||
execution_error: ExecutionErrorWsMessage
|
||||
execution_interrupted: ExecutionInterruptedWsMessage
|
||||
execution_cached: ExecutionCachedWsMessage
|
||||
logs: LogsWsMessage
|
||||
/** Mr Blob Preview, I presume? */
|
||||
@@ -355,6 +357,7 @@ export class ComfyApi extends EventTarget {
|
||||
break
|
||||
case 'execution_start':
|
||||
case 'execution_error':
|
||||
case 'execution_interrupted':
|
||||
case 'execution_cached':
|
||||
case 'execution_success':
|
||||
case 'progress':
|
||||
|
||||
@@ -1478,7 +1478,7 @@ export class ComfyApp {
|
||||
* @returns The workflow and node links
|
||||
*/
|
||||
async graphToPrompt(graph = this.graph, clean = true) {
|
||||
for (const outerNode of this.graph.computeExecutionOrder(false)) {
|
||||
for (const outerNode of graph.computeExecutionOrder(false)) {
|
||||
if (outerNode.widgets) {
|
||||
for (const widget of outerNode.widgets) {
|
||||
// Allow widgets to run callbacks before a prompt has been queued
|
||||
|
||||
@@ -411,223 +411,231 @@ export class ComfyUI {
|
||||
}
|
||||
})
|
||||
|
||||
this.menuContainer = $el('div.comfy-menu', { parent: containerElement }, [
|
||||
$el(
|
||||
'div.drag-handle.comfy-menu-header',
|
||||
{
|
||||
style: {
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
cursor: 'default'
|
||||
}
|
||||
},
|
||||
[
|
||||
$el('span.drag-handle'),
|
||||
$el('span.comfy-menu-queue-size', { $: (q) => (this.queueSize = q) }),
|
||||
$el('div.comfy-menu-actions', [
|
||||
$el('button.comfy-settings-btn', {
|
||||
textContent: '⚙️',
|
||||
onclick: () => {
|
||||
useDialogService().showSettingsDialog()
|
||||
}
|
||||
this.menuContainer = $el(
|
||||
'div.comfy-menu.no-drag',
|
||||
{ parent: containerElement },
|
||||
[
|
||||
$el(
|
||||
'div.drag-handle.comfy-menu-header',
|
||||
{
|
||||
style: {
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
cursor: 'default'
|
||||
}
|
||||
},
|
||||
[
|
||||
$el('span.drag-handle'),
|
||||
$el('span.comfy-menu-queue-size', {
|
||||
$: (q) => (this.queueSize = q)
|
||||
}),
|
||||
$el('button.comfy-close-menu-btn', {
|
||||
textContent: '\u00d7',
|
||||
onclick: () => {
|
||||
useWorkspaceStore().focusMode = true
|
||||
$el('div.comfy-menu-actions', [
|
||||
$el('button.comfy-settings-btn', {
|
||||
textContent: '⚙️',
|
||||
onclick: () => {
|
||||
useDialogService().showSettingsDialog()
|
||||
}
|
||||
}),
|
||||
$el('button.comfy-close-menu-btn', {
|
||||
textContent: '\u00d7',
|
||||
onclick: () => {
|
||||
useWorkspaceStore().focusMode = true
|
||||
}
|
||||
})
|
||||
])
|
||||
]
|
||||
),
|
||||
$el('button.comfy-queue-btn', {
|
||||
id: 'queue-button',
|
||||
textContent: 'Queue Prompt',
|
||||
onclick: () => app.queuePrompt(0, this.batchCount)
|
||||
}),
|
||||
$el('div', {}, [
|
||||
$el('label', { innerHTML: 'Extra options' }, [
|
||||
$el('input', {
|
||||
type: 'checkbox',
|
||||
onchange: (i) => {
|
||||
document.getElementById('extraOptions').style.display = i
|
||||
.srcElement.checked
|
||||
? 'block'
|
||||
: 'none'
|
||||
this.batchCount = i.srcElement.checked
|
||||
? Number.parseInt(
|
||||
(
|
||||
document.getElementById(
|
||||
'batchCountInputRange'
|
||||
) as HTMLInputElement
|
||||
).value
|
||||
)
|
||||
: 1
|
||||
;(
|
||||
document.getElementById(
|
||||
'autoQueueCheckbox'
|
||||
) as HTMLInputElement
|
||||
).checked = false
|
||||
this.autoQueueEnabled = false
|
||||
}
|
||||
})
|
||||
])
|
||||
]
|
||||
),
|
||||
$el('button.comfy-queue-btn', {
|
||||
id: 'queue-button',
|
||||
textContent: 'Queue Prompt',
|
||||
onclick: () => app.queuePrompt(0, this.batchCount)
|
||||
}),
|
||||
$el('div', {}, [
|
||||
$el('label', { innerHTML: 'Extra options' }, [
|
||||
$el('input', {
|
||||
type: 'checkbox',
|
||||
onchange: (i) => {
|
||||
document.getElementById('extraOptions').style.display = i
|
||||
.srcElement.checked
|
||||
? 'block'
|
||||
: 'none'
|
||||
this.batchCount = i.srcElement.checked
|
||||
? Number.parseInt(
|
||||
(
|
||||
document.getElementById(
|
||||
'batchCountInputRange'
|
||||
) as HTMLInputElement
|
||||
).value
|
||||
)
|
||||
: 1
|
||||
;(
|
||||
document.getElementById('autoQueueCheckbox') as HTMLInputElement
|
||||
).checked = false
|
||||
this.autoQueueEnabled = false
|
||||
}
|
||||
})
|
||||
])
|
||||
]),
|
||||
$el(
|
||||
'div',
|
||||
{ id: 'extraOptions', style: { width: '100%', display: 'none' } },
|
||||
[
|
||||
$el('div', [
|
||||
$el('label', { innerHTML: 'Batch count' }),
|
||||
$el('input', {
|
||||
id: 'batchCountInputNumber',
|
||||
type: 'number',
|
||||
value: this.batchCount,
|
||||
min: '1',
|
||||
style: { width: '35%', marginLeft: '0.4em' },
|
||||
oninput: (i) => {
|
||||
this.batchCount = i.target.value
|
||||
/* Even though an <input> element with a type of range logically represents a number (since
|
||||
]),
|
||||
$el(
|
||||
'div',
|
||||
{ id: 'extraOptions', style: { width: '100%', display: 'none' } },
|
||||
[
|
||||
$el('div', [
|
||||
$el('label', { innerHTML: 'Batch count' }),
|
||||
$el('input', {
|
||||
id: 'batchCountInputNumber',
|
||||
type: 'number',
|
||||
value: this.batchCount,
|
||||
min: '1',
|
||||
style: { width: '35%', marginLeft: '0.4em' },
|
||||
oninput: (i) => {
|
||||
this.batchCount = i.target.value
|
||||
/* Even though an <input> element with a type of range logically represents a number (since
|
||||
it's used for numeric input), the value it holds is still treated as a string in HTML and
|
||||
JavaScript. This behavior is consistent across all <input> elements regardless of their type
|
||||
(like text, number, or range), where the .value property is always a string. */
|
||||
;(
|
||||
document.getElementById(
|
||||
'batchCountInputRange'
|
||||
) as HTMLInputElement
|
||||
).value = this.batchCount.toString()
|
||||
}
|
||||
}),
|
||||
$el('input', {
|
||||
id: 'batchCountInputRange',
|
||||
type: 'range',
|
||||
min: '1',
|
||||
max: '100',
|
||||
value: this.batchCount,
|
||||
oninput: (i) => {
|
||||
this.batchCount = i.srcElement.value
|
||||
// Note
|
||||
;(
|
||||
document.getElementById(
|
||||
'batchCountInputNumber'
|
||||
) as HTMLInputElement
|
||||
).value = i.srcElement.value
|
||||
}
|
||||
})
|
||||
]),
|
||||
$el('div', [
|
||||
$el('label', {
|
||||
for: 'autoQueueCheckbox',
|
||||
innerHTML: 'Auto Queue'
|
||||
}),
|
||||
$el('input', {
|
||||
id: 'autoQueueCheckbox',
|
||||
type: 'checkbox',
|
||||
checked: false,
|
||||
title: 'Automatically queue prompt when the queue size hits 0',
|
||||
onchange: (e) => {
|
||||
this.autoQueueEnabled = e.target.checked
|
||||
autoQueueModeEl.style.display = this.autoQueueEnabled
|
||||
? ''
|
||||
: 'none'
|
||||
}
|
||||
}),
|
||||
autoQueueModeEl
|
||||
])
|
||||
]
|
||||
),
|
||||
$el('div.comfy-menu-btns', [
|
||||
;(
|
||||
document.getElementById(
|
||||
'batchCountInputRange'
|
||||
) as HTMLInputElement
|
||||
).value = this.batchCount.toString()
|
||||
}
|
||||
}),
|
||||
$el('input', {
|
||||
id: 'batchCountInputRange',
|
||||
type: 'range',
|
||||
min: '1',
|
||||
max: '100',
|
||||
value: this.batchCount,
|
||||
oninput: (i) => {
|
||||
this.batchCount = i.srcElement.value
|
||||
// Note
|
||||
;(
|
||||
document.getElementById(
|
||||
'batchCountInputNumber'
|
||||
) as HTMLInputElement
|
||||
).value = i.srcElement.value
|
||||
}
|
||||
})
|
||||
]),
|
||||
$el('div', [
|
||||
$el('label', {
|
||||
for: 'autoQueueCheckbox',
|
||||
innerHTML: 'Auto Queue'
|
||||
}),
|
||||
$el('input', {
|
||||
id: 'autoQueueCheckbox',
|
||||
type: 'checkbox',
|
||||
checked: false,
|
||||
title: 'Automatically queue prompt when the queue size hits 0',
|
||||
onchange: (e) => {
|
||||
this.autoQueueEnabled = e.target.checked
|
||||
autoQueueModeEl.style.display = this.autoQueueEnabled
|
||||
? ''
|
||||
: 'none'
|
||||
}
|
||||
}),
|
||||
autoQueueModeEl
|
||||
])
|
||||
]
|
||||
),
|
||||
$el('div.comfy-menu-btns', [
|
||||
$el('button', {
|
||||
id: 'queue-front-button',
|
||||
textContent: 'Queue Front',
|
||||
onclick: () => app.queuePrompt(-1, this.batchCount)
|
||||
}),
|
||||
$el('button', {
|
||||
$: (b) => (this.queue.button = b as HTMLButtonElement),
|
||||
id: 'comfy-view-queue-button',
|
||||
textContent: 'View Queue',
|
||||
onclick: () => {
|
||||
this.history.hide()
|
||||
this.queue.toggle()
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
$: (b) => (this.history.button = b as HTMLButtonElement),
|
||||
id: 'comfy-view-history-button',
|
||||
textContent: 'View History',
|
||||
onclick: () => {
|
||||
this.queue.hide()
|
||||
this.history.toggle()
|
||||
}
|
||||
})
|
||||
]),
|
||||
this.queue.element,
|
||||
this.history.element,
|
||||
$el('button', {
|
||||
id: 'queue-front-button',
|
||||
textContent: 'Queue Front',
|
||||
onclick: () => app.queuePrompt(-1, this.batchCount)
|
||||
}),
|
||||
$el('button', {
|
||||
$: (b) => (this.queue.button = b as HTMLButtonElement),
|
||||
id: 'comfy-view-queue-button',
|
||||
textContent: 'View Queue',
|
||||
id: 'comfy-save-button',
|
||||
textContent: 'Save',
|
||||
onclick: () => {
|
||||
this.history.hide()
|
||||
this.queue.toggle()
|
||||
useCommandStore().execute('Comfy.ExportWorkflow')
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
$: (b) => (this.history.button = b as HTMLButtonElement),
|
||||
id: 'comfy-view-history-button',
|
||||
textContent: 'View History',
|
||||
id: 'comfy-dev-save-api-button',
|
||||
textContent: 'Save (API Format)',
|
||||
style: { width: '100%', display: 'none' },
|
||||
onclick: () => {
|
||||
this.queue.hide()
|
||||
this.history.toggle()
|
||||
useCommandStore().execute('Comfy.ExportWorkflowAPI')
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-load-button',
|
||||
textContent: 'Load',
|
||||
onclick: () => fileInput.click()
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-refresh-button',
|
||||
textContent: 'Refresh',
|
||||
onclick: () => app.refreshComboInNodes()
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-clipspace-button',
|
||||
textContent: 'Clipspace',
|
||||
onclick: () => app.openClipspace()
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-clear-button',
|
||||
textContent: 'Clear',
|
||||
onclick: () => {
|
||||
if (
|
||||
!useSettingStore().get('Comfy.ConfirmClear') ||
|
||||
confirm('Clear workflow?')
|
||||
) {
|
||||
app.clean()
|
||||
app.graph.clear()
|
||||
app.resetView()
|
||||
api.dispatchCustomEvent('graphCleared')
|
||||
}
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-load-default-button',
|
||||
textContent: 'Load Default',
|
||||
onclick: async () => {
|
||||
if (
|
||||
!useSettingStore().get('Comfy.ConfirmClear') ||
|
||||
confirm('Load default workflow?')
|
||||
) {
|
||||
app.resetView()
|
||||
await app.loadGraphData()
|
||||
}
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-reset-view-button',
|
||||
textContent: 'Reset View',
|
||||
onclick: async () => {
|
||||
app.resetView()
|
||||
}
|
||||
})
|
||||
]),
|
||||
this.queue.element,
|
||||
this.history.element,
|
||||
$el('button', {
|
||||
id: 'comfy-save-button',
|
||||
textContent: 'Save',
|
||||
onclick: () => {
|
||||
useCommandStore().execute('Comfy.ExportWorkflow')
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-dev-save-api-button',
|
||||
textContent: 'Save (API Format)',
|
||||
style: { width: '100%', display: 'none' },
|
||||
onclick: () => {
|
||||
useCommandStore().execute('Comfy.ExportWorkflowAPI')
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-load-button',
|
||||
textContent: 'Load',
|
||||
onclick: () => fileInput.click()
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-refresh-button',
|
||||
textContent: 'Refresh',
|
||||
onclick: () => app.refreshComboInNodes()
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-clipspace-button',
|
||||
textContent: 'Clipspace',
|
||||
onclick: () => app.openClipspace()
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-clear-button',
|
||||
textContent: 'Clear',
|
||||
onclick: () => {
|
||||
if (
|
||||
!useSettingStore().get('Comfy.ConfirmClear') ||
|
||||
confirm('Clear workflow?')
|
||||
) {
|
||||
app.clean()
|
||||
app.graph.clear()
|
||||
app.resetView()
|
||||
api.dispatchCustomEvent('graphCleared')
|
||||
}
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-load-default-button',
|
||||
textContent: 'Load Default',
|
||||
onclick: async () => {
|
||||
if (
|
||||
!useSettingStore().get('Comfy.ConfirmClear') ||
|
||||
confirm('Load default workflow?')
|
||||
) {
|
||||
app.resetView()
|
||||
await app.loadGraphData()
|
||||
}
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-reset-view-button',
|
||||
textContent: 'Reset View',
|
||||
onclick: async () => {
|
||||
app.resetView()
|
||||
}
|
||||
})
|
||||
]) as HTMLDivElement
|
||||
]
|
||||
) as HTMLDivElement
|
||||
// Hide by default on construction so it does not interfere with other views.
|
||||
this.menuContainer.style.display = 'none'
|
||||
|
||||
|
||||
@@ -176,10 +176,6 @@ export const useWorkflowService = () => {
|
||||
workflow: ComfyWorkflow,
|
||||
options: { warnIfUnsaved: boolean } = { warnIfUnsaved: true }
|
||||
): Promise<boolean> => {
|
||||
if (!workflow.isLoaded) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (workflow.isModified && options.warnIfUnsaved) {
|
||||
const confirmed = await dialogService.confirm({
|
||||
title: t('sideToolbar.workflowTab.dirtyCloseTitle'),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import { defineStore } from 'pinia'
|
||||
import { Ref, computed, ref, toRaw } from 'vue'
|
||||
|
||||
@@ -145,20 +146,9 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
return keybindingByKeyCombo.value[combo.serialize()]
|
||||
}
|
||||
|
||||
function createKeybindingsByCommandId(keybindings: KeybindingImpl[]) {
|
||||
const result: Record<string, KeybindingImpl[]> = {}
|
||||
for (const keybinding of keybindings) {
|
||||
if (!(keybinding.commandId in result)) {
|
||||
result[keybinding.commandId] = []
|
||||
}
|
||||
result[keybinding.commandId].push(keybinding)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const keybindingsByCommandId = computed<Record<string, KeybindingImpl[]>>(
|
||||
() => {
|
||||
return createKeybindingsByCommandId(keybindings.value)
|
||||
return _.groupBy(keybindings.value, 'commandId')
|
||||
}
|
||||
)
|
||||
|
||||
@@ -169,7 +159,7 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
const defaultKeybindingsByCommandId = computed<
|
||||
Record<string, KeybindingImpl[]>
|
||||
>(() => {
|
||||
return createKeybindingsByCommandId(Object.values(defaultKeybindings.value))
|
||||
return _.groupBy(Object.values(defaultKeybindings.value), 'commandId')
|
||||
})
|
||||
|
||||
function getKeybindingByCommandId(commandId: string) {
|
||||
@@ -237,7 +227,7 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error(`Unknown keybinding: ${JSON.stringify(keybinding)}`)
|
||||
console.warn(`Unset unknown keybinding: ${JSON.stringify(keybinding)}`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -305,9 +305,13 @@ export const SYSTEM_NODE_DEFS: Record<string, ComfyNodeDef> = {
|
||||
}
|
||||
}
|
||||
|
||||
export function buildNodeDefTree(nodeDefs: ComfyNodeDefImpl[]): TreeNode {
|
||||
return buildTree(nodeDefs, (nodeDef: ComfyNodeDefImpl) =>
|
||||
export function buildNodeDefTree(
|
||||
nodeDefs: ComfyNodeDefImpl[],
|
||||
keyFunction: (nodeDef: ComfyNodeDefImpl) => string[] = (nodeDef) =>
|
||||
nodeDef.nodePath.split('/')
|
||||
): TreeNode {
|
||||
return buildTree(nodeDefs, (nodeDef: ComfyNodeDefImpl) =>
|
||||
keyFunction(nodeDef)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -326,12 +330,16 @@ export function createDummyFolderNodeDef(folderPath: string): ComfyNodeDefImpl {
|
||||
} as ComfyNodeDef)
|
||||
}
|
||||
|
||||
const getCategoryKeys = (nodeDef: ComfyNodeDefImpl) =>
|
||||
nodeDef.nodePath.split('/')
|
||||
|
||||
export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
const nodeDefsByName = ref<Record<string, ComfyNodeDefImpl>>({})
|
||||
const nodeDefsByDisplayName = ref<Record<string, ComfyNodeDefImpl>>({})
|
||||
const showDeprecated = ref(false)
|
||||
const showExperimental = ref(false)
|
||||
|
||||
const keyFunction = ref(getCategoryKeys)
|
||||
const nodeDefs = computed(() => Object.values(nodeDefsByName.value))
|
||||
const nodeDataTypes = computed(() => {
|
||||
const types = new Set<string>()
|
||||
@@ -355,7 +363,13 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
const nodeSearchService = computed(
|
||||
() => new NodeSearchService(visibleNodeDefs.value)
|
||||
)
|
||||
const nodeTree = computed(() => buildNodeDefTree(visibleNodeDefs.value))
|
||||
const nodeTree = computed(() =>
|
||||
buildNodeDefTree(visibleNodeDefs.value, keyFunction.value)
|
||||
)
|
||||
|
||||
function setKeyFunction(callback: (nodeDef: ComfyNodeDefImpl) => string[]) {
|
||||
keyFunction.value = callback ?? getCategoryKeys
|
||||
}
|
||||
|
||||
function updateNodeDefs(nodeDefs: ComfyNodeDef[]) {
|
||||
const newNodeDefsByName: Record<string, ComfyNodeDefImpl> = {}
|
||||
@@ -398,7 +412,8 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
|
||||
updateNodeDefs,
|
||||
addNodeDef,
|
||||
fromLGraphNode
|
||||
fromLGraphNode,
|
||||
setKeyFunction
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, markRaw, ref } from 'vue'
|
||||
|
||||
@@ -130,6 +131,10 @@ export interface WorkflowStore {
|
||||
openWorkflows: LoadedComfyWorkflow[]
|
||||
openedWorkflowIndexShift: (shift: number) => LoadedComfyWorkflow | null
|
||||
openWorkflow: (workflow: ComfyWorkflow) => Promise<LoadedComfyWorkflow>
|
||||
openWorkflowsInBackground: (paths: {
|
||||
left?: string[]
|
||||
right?: string[]
|
||||
}) => void
|
||||
isOpen: (workflow: ComfyWorkflow) => boolean
|
||||
isBusy: boolean
|
||||
closeWorkflow: (workflow: ComfyWorkflow) => Promise<void>
|
||||
@@ -213,6 +218,36 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
const isOpen = (workflow: ComfyWorkflow) =>
|
||||
openWorkflowPathSet.value.has(workflow.path)
|
||||
|
||||
/**
|
||||
* Add paths to the list of open workflow paths without loading the files
|
||||
* or changing the active workflow.
|
||||
*
|
||||
* @param paths - The workflows to open, specified as:
|
||||
* - `left`: Workflows to be added to the left.
|
||||
* - `right`: Workflows to be added to the right.
|
||||
*
|
||||
* Invalid paths (non-strings or paths not found in `workflowLookup.value`)
|
||||
* will be ignored. Duplicate paths are automatically removed.
|
||||
*/
|
||||
const openWorkflowsInBackground = (paths: {
|
||||
left?: string[]
|
||||
right?: string[]
|
||||
}) => {
|
||||
const { left = [], right = [] } = paths
|
||||
if (!left.length && !right.length) return
|
||||
|
||||
const isValidPath = (
|
||||
path: unknown
|
||||
): path is keyof typeof workflowLookup.value =>
|
||||
typeof path === 'string' && path in workflowLookup.value
|
||||
|
||||
openWorkflowPaths.value = _.union(
|
||||
left,
|
||||
openWorkflowPaths.value,
|
||||
right
|
||||
).filter(isValidPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the workflow as the active workflow.
|
||||
* @param workflow The workflow to open.
|
||||
@@ -389,6 +424,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
openWorkflows,
|
||||
openedWorkflowIndexShift,
|
||||
openWorkflow,
|
||||
openWorkflowsInBackground,
|
||||
isOpen,
|
||||
isBusy,
|
||||
closeWorkflow,
|
||||
|
||||
@@ -535,7 +535,11 @@ const zSettings = z.record(z.any()).and(
|
||||
'Comfy.Validation.NodeDefs': z.boolean(),
|
||||
'Comfy.Workflow.SortNodeIdOnSave': z.boolean(),
|
||||
'Comfy.Queue.ImageFit': z.enum(['contain', 'cover']),
|
||||
'Comfy.Workflow.WorkflowTabsPosition': z.enum(['Sidebar', 'Topbar']),
|
||||
'Comfy.Workflow.WorkflowTabsPosition': z.enum([
|
||||
'Sidebar',
|
||||
'Topbar',
|
||||
'Topbar (2nd-row)'
|
||||
]),
|
||||
'Comfy.Node.DoubleClickTitleToEdit': z.boolean(),
|
||||
'Comfy.WidgetControlMode': z.enum(['before', 'after']),
|
||||
'Comfy.Window.UnloadConfirmation': z.boolean(),
|
||||
|
||||
@@ -27,6 +27,7 @@ const litegraphBaseSchema = z.object({
|
||||
NODE_SELECTED_TITLE_COLOR: z.string(),
|
||||
NODE_TEXT_SIZE: z.number(),
|
||||
NODE_TEXT_COLOR: z.string(),
|
||||
NODE_TEXT_HIGHLIGHT_COLOR: z.string(),
|
||||
NODE_SUBTEXT_SIZE: z.number(),
|
||||
NODE_DEFAULT_COLOR: z.string(),
|
||||
NODE_DEFAULT_BGCOLOR: z.string(),
|
||||
|
||||
@@ -200,7 +200,7 @@ export const zComfyWorkflow = z
|
||||
.passthrough()
|
||||
|
||||
/** Schema version 1 */
|
||||
const zComfyWorkflow1 = z
|
||||
export const zComfyWorkflow1 = z
|
||||
.object({
|
||||
version: z.literal(1),
|
||||
config: zConfig.optional().nullable(),
|
||||
|
||||
24
src/types/issueReportTypes.ts
Normal file
24
src/types/issueReportTypes.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export type DefaultField = 'Workflow' | 'Logs' | 'SystemStats' | 'Settings'
|
||||
|
||||
export interface ReportField {
|
||||
/**
|
||||
* The label of the field, shown next to the checkbox if the field is opt-in.
|
||||
*/
|
||||
label: string
|
||||
|
||||
/**
|
||||
* A unique identifier for the field, used internally as the key for this field's value.
|
||||
*/
|
||||
value: string
|
||||
|
||||
/**
|
||||
* The data associated with this field, sent as part of the report.
|
||||
*/
|
||||
data: Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Indicates whether the field requires explicit opt-in from the user
|
||||
* before its data is included in the report.
|
||||
*/
|
||||
optIn: boolean
|
||||
}
|
||||
@@ -1,17 +1,16 @@
|
||||
import { ElectronAPI } from '@comfyorg/comfyui-electron-types'
|
||||
|
||||
export type InstallOptions = Parameters<ElectronAPI['installComfyUI']>[0]
|
||||
export type TorchDeviceType = InstallOptions['device']
|
||||
import {
|
||||
ElectronAPI,
|
||||
ElectronContextMenuOptions
|
||||
} from '@comfyorg/comfyui-electron-types'
|
||||
|
||||
export function isElectron() {
|
||||
return 'electronAPI' in window && window['electronAPI'] !== undefined
|
||||
return 'electronAPI' in window && window.electronAPI !== undefined
|
||||
}
|
||||
|
||||
export function electronAPI() {
|
||||
return (window as any)['electronAPI'] as ElectronAPI
|
||||
return (window as any).electronAPI as ElectronAPI
|
||||
}
|
||||
|
||||
type NativeContextOptions = Parameters<ElectronAPI['showContextMenu']>[0]
|
||||
export function showNativeMenu(options?: NativeContextOptions) {
|
||||
export function showNativeMenu(options?: ElectronContextMenuOptions) {
|
||||
electronAPI()?.showContextMenu(options)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import GlobalToast from '@/components/toast/GlobalToast.vue'
|
||||
import TopMenubar from '@/components/topbar/TopMenubar.vue'
|
||||
import { SERVER_CONFIG_ITEMS } from '@/constants/serverConfig'
|
||||
import { useCoreCommands } from '@/hooks/coreCommandHooks'
|
||||
import { useErrorHandling } from '@/hooks/errorHooks'
|
||||
import { i18n } from '@/i18n'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
@@ -45,6 +46,7 @@ import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { StatusWsMessageStatus } from '@/types/apiTypes'
|
||||
import { electronAPI, isElectron } from '@/utils/envUtil'
|
||||
|
||||
setupAutoQueueHandler()
|
||||
|
||||
@@ -63,6 +65,13 @@ watch(
|
||||
} else {
|
||||
document.body.classList.add(DARK_THEME_CLASS)
|
||||
}
|
||||
|
||||
if (isElectron()) {
|
||||
electronAPI().changeTheme({
|
||||
color: 'rgba(0, 0, 0, 0)',
|
||||
symbolColor: newTheme.colors.comfy_base['input-text']
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
@@ -162,29 +171,30 @@ onBeforeUnmount(() => {
|
||||
|
||||
useEventListener(window, 'keydown', useKeybindingService().keybindHandler)
|
||||
|
||||
const { wrapWithErrorHandling, wrapWithErrorHandlingAsync } = useErrorHandling()
|
||||
const onGraphReady = () => {
|
||||
requestIdleCallback(
|
||||
() => {
|
||||
// Setting values now available after comfyApp.setup.
|
||||
// Load keybindings.
|
||||
useKeybindingService().registerUserKeybindings()
|
||||
wrapWithErrorHandling(useKeybindingService().registerUserKeybindings)()
|
||||
|
||||
// Load server config
|
||||
useServerConfigStore().loadServerConfig(
|
||||
wrapWithErrorHandling(useServerConfigStore().loadServerConfig)(
|
||||
SERVER_CONFIG_ITEMS,
|
||||
settingStore.get('Comfy.Server.ServerConfigValues')
|
||||
)
|
||||
|
||||
// Load model folders
|
||||
useModelStore().loadModelFolders()
|
||||
wrapWithErrorHandlingAsync(useModelStore().loadModelFolders)()
|
||||
|
||||
// Non-blocking load of node frequencies
|
||||
wrapWithErrorHandlingAsync(useNodeFrequencyStore().loadNodeFrequencies)()
|
||||
|
||||
// Node defs now available after comfyApp.setup.
|
||||
// Explicitly initialize nodeSearchService to avoid indexing delay when
|
||||
// node search is triggered
|
||||
useNodeDefStore().nodeSearchService.endsWithFilterStartSequence('')
|
||||
|
||||
// Non-blocking load of node frequencies
|
||||
useNodeFrequencyStore().loadNodeFrequencies()
|
||||
},
|
||||
{ timeout: 1000 }
|
||||
)
|
||||
|
||||
@@ -103,6 +103,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
InstallOptions,
|
||||
TorchDeviceType
|
||||
} from '@comfyorg/comfyui-electron-types'
|
||||
import Button from 'primevue/button'
|
||||
import Step from 'primevue/step'
|
||||
import StepList from 'primevue/steplist'
|
||||
@@ -116,11 +120,7 @@ import DesktopSettingsConfiguration from '@/components/install/DesktopSettingsCo
|
||||
import GpuPicker from '@/components/install/GpuPicker.vue'
|
||||
import InstallLocationPicker from '@/components/install/InstallLocationPicker.vue'
|
||||
import MigrationPicker from '@/components/install/MigrationPicker.vue'
|
||||
import {
|
||||
type InstallOptions,
|
||||
type TorchDeviceType,
|
||||
electronAPI
|
||||
} from '@/utils/envUtil'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import BaseViewTemplate from '@/views/templates/BaseViewTemplate.vue'
|
||||
|
||||
const device = ref<TorchDeviceType>(null)
|
||||
|
||||
@@ -1,43 +1,45 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark class="flex-col">
|
||||
<h2 class="text-2xl font-bold">
|
||||
{{ t(`serverStart.process.${status}`) }}
|
||||
<span v-if="status === ProgressStatus.ERROR">
|
||||
v{{ electronVersion }}
|
||||
</span>
|
||||
</h2>
|
||||
<div
|
||||
v-if="status === ProgressStatus.ERROR"
|
||||
class="flex flex-col items-center gap-4"
|
||||
>
|
||||
<div class="flex items-center my-4 gap-2">
|
||||
<div class="flex flex-col w-full h-full items-center">
|
||||
<h2 class="text-2xl font-bold">
|
||||
{{ t(`serverStart.process.${status}`) }}
|
||||
<span v-if="status === ProgressStatus.ERROR">
|
||||
v{{ electronVersion }}
|
||||
</span>
|
||||
</h2>
|
||||
<div
|
||||
v-if="status === ProgressStatus.ERROR"
|
||||
class="flex flex-col items-center gap-4"
|
||||
>
|
||||
<div class="flex items-center my-4 gap-2">
|
||||
<Button
|
||||
icon="pi pi-flag"
|
||||
severity="secondary"
|
||||
:label="t('serverStart.reportIssue')"
|
||||
@click="reportIssue"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-file"
|
||||
severity="secondary"
|
||||
:label="t('serverStart.openLogs')"
|
||||
@click="openLogs"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
:label="t('serverStart.reinstall')"
|
||||
@click="reinstall"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
icon="pi pi-flag"
|
||||
v-if="!terminalVisible"
|
||||
icon="pi pi-search"
|
||||
severity="secondary"
|
||||
:label="t('serverStart.reportIssue')"
|
||||
@click="reportIssue"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-file"
|
||||
severity="secondary"
|
||||
:label="t('serverStart.openLogs')"
|
||||
@click="openLogs"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
:label="t('serverStart.reinstall')"
|
||||
@click="reinstall"
|
||||
:label="t('serverStart.showTerminal')"
|
||||
@click="terminalVisible = true"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
v-if="!terminalVisible"
|
||||
icon="pi pi-search"
|
||||
severity="secondary"
|
||||
:label="t('serverStart.showTerminal')"
|
||||
@click="terminalVisible = true"
|
||||
/>
|
||||
<BaseTerminal v-show="terminalVisible" @created="terminalCreated" />
|
||||
</div>
|
||||
<BaseTerminal v-show="terminalVisible" @created="terminalCreated" />
|
||||
</BaseViewTemplate>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
<template>
|
||||
<div
|
||||
class="font-sans w-screen h-screen flex items-center justify-center pointer-events-auto overflow-auto"
|
||||
class="font-sans w-screen h-screen flex flex-col pointer-events-auto"
|
||||
:class="[
|
||||
props.dark
|
||||
? 'text-neutral-300 bg-neutral-900 dark-theme'
|
||||
: 'text-neutral-900 bg-neutral-300'
|
||||
]"
|
||||
>
|
||||
<slot></slot>
|
||||
<!-- Virtual top menu for native window (drag handle) -->
|
||||
<div
|
||||
v-show="isNativeWindow"
|
||||
ref="topMenuRef"
|
||||
class="app-drag w-full h-[var(--comfy-topbar-height)]"
|
||||
/>
|
||||
<div
|
||||
class="flex-grow w-full flex items-center justify-center overflow-auto"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
|
||||
import { electronAPI, isElectron } from '@/utils/envUtil'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
dark: boolean
|
||||
@@ -20,4 +34,30 @@ const props = withDefaults(
|
||||
dark: false
|
||||
}
|
||||
)
|
||||
|
||||
const darkTheme = {
|
||||
color: 'rgba(0, 0, 0, 0)',
|
||||
symbolColor: '#d4d4d4'
|
||||
}
|
||||
|
||||
const lightTheme = {
|
||||
color: 'rgba(0, 0, 0, 0)',
|
||||
symbolColor: '#171717'
|
||||
}
|
||||
|
||||
const topMenuRef = ref<HTMLDivElement | null>(null)
|
||||
const isNativeWindow = ref(false)
|
||||
onMounted(async () => {
|
||||
if (isElectron()) {
|
||||
const windowStyle = await electronAPI().Config.getWindowStyle()
|
||||
isNativeWindow.value = windowStyle === 'custom'
|
||||
|
||||
await nextTick()
|
||||
|
||||
electronAPI().changeTheme({
|
||||
...(props.dark ? darkTheme : lightTheme),
|
||||
height: topMenuRef.value.getBoundingClientRect().height
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -33,6 +33,18 @@ describe('useKeybindingStore', () => {
|
||||
expect(store.getKeybinding(keybinding.combo)).toEqual(keybinding)
|
||||
})
|
||||
|
||||
it('should get keybindings by command id', () => {
|
||||
const store = useKeybindingStore()
|
||||
const keybinding = new KeybindingImpl({
|
||||
commandId: 'test.command',
|
||||
combo: { key: 'C', ctrl: true }
|
||||
})
|
||||
store.addDefaultKeybinding(keybinding)
|
||||
expect(store.getKeybindingsByCommandId('test.command')).toEqual([
|
||||
keybinding
|
||||
])
|
||||
})
|
||||
|
||||
it('should override default keybindings with user keybindings', () => {
|
||||
const store = useKeybindingStore()
|
||||
const defaultKeybinding = new KeybindingImpl({
|
||||
@@ -137,6 +149,24 @@ describe('useKeybindingStore', () => {
|
||||
expect(() => store.unsetKeybinding(keybinding)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should not throw an error when unsetting unknown keybinding', () => {
|
||||
const store = useKeybindingStore()
|
||||
const keybinding = new KeybindingImpl({
|
||||
commandId: 'test.command',
|
||||
combo: { key: 'I', ctrl: true }
|
||||
})
|
||||
store.addUserKeybinding(keybinding)
|
||||
|
||||
expect(() =>
|
||||
store.unsetKeybinding(
|
||||
new KeybindingImpl({
|
||||
commandId: 'test.foo',
|
||||
combo: { key: 'I', ctrl: true }
|
||||
})
|
||||
)
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('should remove unset keybinding when adding back a default keybinding', () => {
|
||||
const store = useKeybindingStore()
|
||||
const defaultKeybinding = new KeybindingImpl({
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createPinia, setActivePinia } from 'pinia'
|
||||
import { api } from '@/scripts/api'
|
||||
import { defaultGraph, defaultGraphJSON } from '@/scripts/defaultGraph'
|
||||
import {
|
||||
ComfyWorkflow,
|
||||
LoadedComfyWorkflow,
|
||||
useWorkflowBookmarkStore,
|
||||
useWorkflowStore
|
||||
@@ -161,6 +162,97 @@ describe('useWorkflowStore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('openWorkflowsInBackground', () => {
|
||||
let workflowA: ComfyWorkflow
|
||||
let workflowB: ComfyWorkflow
|
||||
let workflowC: ComfyWorkflow
|
||||
|
||||
const openWorkflowPaths = () =>
|
||||
store.openWorkflows.filter((w) => store.isOpen(w)).map((w) => w.path)
|
||||
|
||||
beforeEach(async () => {
|
||||
await syncRemoteWorkflows(['a.json', 'b.json', 'c.json'])
|
||||
workflowA = store.getWorkflowByPath('workflows/a.json')!
|
||||
workflowB = store.getWorkflowByPath('workflows/b.json')!
|
||||
workflowC = store.getWorkflowByPath('workflows/c.json')!
|
||||
;(api.getUserData as jest.Mock).mockResolvedValue({
|
||||
status: 200,
|
||||
text: () => Promise.resolve(defaultGraphJSON)
|
||||
})
|
||||
})
|
||||
|
||||
it('should open workflows adjacent to the active workflow', async () => {
|
||||
await store.openWorkflow(workflowA)
|
||||
store.openWorkflowsInBackground({
|
||||
left: [workflowB.path],
|
||||
right: [workflowC.path]
|
||||
})
|
||||
expect(openWorkflowPaths()).toEqual([
|
||||
workflowB.path,
|
||||
workflowA.path,
|
||||
workflowC.path
|
||||
])
|
||||
})
|
||||
|
||||
it('should not change the active workflow', async () => {
|
||||
await store.openWorkflow(workflowA)
|
||||
store.openWorkflowsInBackground({
|
||||
left: [workflowC.path],
|
||||
right: [workflowB.path]
|
||||
})
|
||||
expect(store.activeWorkflow).not.toBeUndefined()
|
||||
expect(store.activeWorkflow!.path).toBe(workflowA.path)
|
||||
})
|
||||
|
||||
it('should open workflows when none are active', async () => {
|
||||
expect(store.openWorkflows.length).toBe(0)
|
||||
store.openWorkflowsInBackground({
|
||||
left: [workflowA.path],
|
||||
right: [workflowB.path]
|
||||
})
|
||||
expect(openWorkflowPaths()).toEqual([workflowA.path, workflowB.path])
|
||||
})
|
||||
|
||||
it('should not open duplicate workflows', async () => {
|
||||
store.openWorkflowsInBackground({
|
||||
left: [workflowA.path, workflowB.path, workflowA.path],
|
||||
right: [workflowB.path, workflowA.path, workflowB.path]
|
||||
})
|
||||
expect(openWorkflowPaths()).toEqual([workflowA.path, workflowB.path])
|
||||
})
|
||||
|
||||
it('should not open workflow that is already open', async () => {
|
||||
await store.openWorkflow(workflowA)
|
||||
store.openWorkflowsInBackground({
|
||||
left: [workflowA.path]
|
||||
})
|
||||
expect(openWorkflowPaths()).toEqual([workflowA.path])
|
||||
expect(store.activeWorkflow?.path).toBe(workflowA.path)
|
||||
})
|
||||
|
||||
it('should ignore invalid or deleted workflow paths', async () => {
|
||||
await store.openWorkflow(workflowA)
|
||||
store.openWorkflowsInBackground({
|
||||
left: ['workflows/invalid::$-path.json'],
|
||||
right: ['workflows/deleted-since-last-session.json']
|
||||
})
|
||||
expect(openWorkflowPaths()).toEqual([workflowA.path])
|
||||
expect(store.activeWorkflow?.path).toBe(workflowA.path)
|
||||
})
|
||||
|
||||
it('should do nothing when given an empty argument', async () => {
|
||||
await store.openWorkflow(workflowA)
|
||||
|
||||
store.openWorkflowsInBackground({})
|
||||
expect(openWorkflowPaths()).toEqual([workflowA.path])
|
||||
|
||||
store.openWorkflowsInBackground({ left: [], right: [] })
|
||||
expect(openWorkflowPaths()).toEqual([workflowA.path])
|
||||
|
||||
expect(store.activeWorkflow?.path).toBe(workflowA.path)
|
||||
})
|
||||
})
|
||||
|
||||
describe('renameWorkflow', () => {
|
||||
it('should rename workflow and update bookmarks', async () => {
|
||||
const workflow = store.createTemporary('dir/test.json')
|
||||
|
||||
@@ -178,7 +178,11 @@ export default defineConfig({
|
||||
define: {
|
||||
__COMFYUI_FRONTEND_VERSION__: JSON.stringify(
|
||||
process.env.npm_package_version
|
||||
)
|
||||
),
|
||||
__SENTRY_ENABLED__: JSON.stringify(
|
||||
!(process.env.NODE_ENV === 'development' || !process.env.SENTRY_DSN)
|
||||
),
|
||||
__SENTRY_DSN__: JSON.stringify(process.env.SENTRY_DSN || '')
|
||||
},
|
||||
|
||||
resolve: {
|
||||
|
||||
@@ -53,7 +53,12 @@ const mockElectronAPI: Plugin = {
|
||||
},
|
||||
getElectronVersion: () => Promise.resolve('1.0.0'),
|
||||
getComfyUIVersion: () => '9.9.9',
|
||||
getPlatform: () => 'win32'
|
||||
getPlatform: () => 'win32',
|
||||
changeTheme: () => {},
|
||||
Config: {
|
||||
setWindowStyle: () => {},
|
||||
getWindowStyle: () => Promise.resolve('default')
|
||||
}
|
||||
};`
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user