Compare commits

..

2 Commits

Author SHA1 Message Date
pythongosssss
02c13c403f Safety mechanism to prevent loading everything 2024-11-24 11:44:26 +00:00
pythongosssss
8254e3c9cf Fix number of items shown when loading more
Fix number of items shown on status update
2024-11-24 11:37:02 +00:00
170 changed files with 2178 additions and 9745 deletions

View File

@@ -41,21 +41,3 @@ jobs:
draft: true
prerelease: false
make_latest: "true"
publish_types:
runs-on: ubuntu-latest
if: >
github.event.pull_request.merged == true &&
contains(github.event.pull_request.labels.*.name, 'Release')
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: lts/*
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npm run build:types
- name: Publish package
run: npm publish --access public
working-directory: ./dist
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,11 +0,0 @@
// This file is intentionally kept in CommonJS format (.cjs)
// to resolve compatibility issues with dependencies that require CommonJS.
// Do not convert this file to ESModule format unless all dependencies support it.
const { defineConfig } = require('@lobehub/i18n-cli');
module.exports = defineConfig({
entry: 'src/locales/en.json',
entryLocale: 'en',
output: 'src/locales',
outputLocales: ['zh', 'ru', 'ja'],
});

View File

@@ -414,7 +414,6 @@ We will support custom icons later.
- [PrimeVue](https://primevue.org/) with [TailwindCSS](https://tailwindcss.com/) for UI
- [Litegraph](https://github.com/Comfy-Org/litegraph.js) for node editor
- [zod](https://zod.dev/) for schema validation
- [vue-i18n](https://github.com/intlify/vue-i18n) for internationalization
### Git pre-commit hooks
@@ -481,77 +480,6 @@ This repo is using litegraph package hosted on <https://github.com/Comfy-Org/lit
This will replace the litegraph package in this repo with the local litegraph repo.
## Internationalization (i18n)
Our project supports multiple languages using `vue-i18n`. This allows users around the world to use the application in their preferred language.
### Supported Languages
- en
- zh
- ru
- ja
### How to Add a New Language
We welcome the addition of new languages. You can add a new language by following these steps:
#### 1. Generate language files
We use [lobe-i18n](https://github.com/lobehub/lobe-cli-toolbox/blob/master/packages/lobe-i18n/README.md) as our translation tool, which integrates with LLM for efficient localization.
Update the configuration file to include the new language(s) you wish to add:
```javascript
const { defineConfig } = require('@lobehub/i18n-cli');
module.exports = defineConfig({
entry: 'src/locales/en.json', // Base language file
entryLocale: 'en',
output: 'src/locales',
outputLocales: ['zh', 'ru', 'ja'], // Add the new language(s) here
});
```
Set your OpenAI API Key by running the following command:
```sh
npx lobe-i18n --option
```
Once configured, generate the translation files with:
```sh
npx lobe-i18n locale
```
This will create the language files for the specified languages in the configuration.
#### 2. Update i18n Configuration
Import the newly generated locale file(s) in the `src/i18n.ts` file to include them in the application's i18n setup.
#### 3. Enable Selection of the New Language
Add the newly added language to the following item in `src/constants/coreSettings.ts`:
```typescript
{
id: 'Comfy.Locale',
name: 'Locale',
type: 'combo',
options: ['en', 'zh', 'ru', 'ja'], // Add the new language(s) here
defaultValue: navigator.language.split('-')[0] || 'en'
},
```
This will make the new language selectable in the application's settings.
#### 4. Test the Translations
Start the development server, switch to the new language, and verify the translations.
You can switch languages by opening the ComfyUI Settings and selecting from the `ComfyUI > Locale` dropdown box.
## Deploy
- Option 1: Set `DEPLOY_COMFYUI_DIR` in `.env` and run `npm run deploy`.

View File

@@ -3,6 +3,9 @@ import {
comfyPageFixture as test,
comfyExpect as expect
} from './fixtures/ComfyPage'
import type { useWorkspaceStore } from '../src/stores/workspaceStore'
type WorkspaceStore = ReturnType<typeof useWorkspaceStore>
async function beforeChange(comfyPage: ComfyPage) {
await comfyPage.page.evaluate(() => {
@@ -23,41 +26,64 @@ test.describe('Change Tracker', () => {
})
test('Can undo multiple operations', async ({ comfyPage }) => {
expect(await comfyPage.getUndoQueueSize()).toBe(0)
expect(await comfyPage.getRedoQueueSize()).toBe(0)
function isModified() {
return comfyPage.page.evaluate(async () => {
return !!(window['app'].extensionManager as WorkspaceStore).workflow
.activeWorkflow?.isModified
})
}
function getUndoQueueSize() {
return comfyPage.page.evaluate(() => {
const workflow = (window['app'].extensionManager as WorkspaceStore)
.workflow.activeWorkflow
return workflow?.changeTracker.undoQueue.length
})
}
function getRedoQueueSize() {
return comfyPage.page.evaluate(() => {
const workflow = (window['app'].extensionManager as WorkspaceStore)
.workflow.activeWorkflow
return workflow?.changeTracker.redoQueue.length
})
}
expect(await getUndoQueueSize()).toBe(0)
expect(await getRedoQueueSize()).toBe(0)
// Save, confirm no errors & workflow modified flag removed
await comfyPage.menu.topbar.saveWorkflow('undo-redo-test')
expect(await comfyPage.getToastErrorCount()).toBe(0)
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
expect(await comfyPage.getUndoQueueSize()).toBe(0)
expect(await comfyPage.getRedoQueueSize()).toBe(0)
expect(await isModified()).toBe(false)
expect(await getUndoQueueSize()).toBe(0)
expect(await getRedoQueueSize()).toBe(0)
const node = (await comfyPage.getFirstNodeRef())!
await node.click('title')
await node.click('collapse')
await expect(node).toBeCollapsed()
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
expect(await comfyPage.getUndoQueueSize()).toBe(1)
expect(await comfyPage.getRedoQueueSize()).toBe(0)
expect(await isModified()).toBe(true)
expect(await getUndoQueueSize()).toBe(1)
expect(await getRedoQueueSize()).toBe(0)
await comfyPage.ctrlB()
await expect(node).toBeBypassed()
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
expect(await comfyPage.getUndoQueueSize()).toBe(2)
expect(await comfyPage.getRedoQueueSize()).toBe(0)
expect(await isModified()).toBe(true)
expect(await getUndoQueueSize()).toBe(2)
expect(await getRedoQueueSize()).toBe(0)
await comfyPage.ctrlZ()
await expect(node).not.toBeBypassed()
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
expect(await comfyPage.getUndoQueueSize()).toBe(1)
expect(await comfyPage.getRedoQueueSize()).toBe(1)
expect(await isModified()).toBe(true)
expect(await getUndoQueueSize()).toBe(1)
expect(await getRedoQueueSize()).toBe(1)
await comfyPage.ctrlZ()
await expect(node).not.toBeCollapsed()
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
expect(await comfyPage.getUndoQueueSize()).toBe(0)
expect(await comfyPage.getRedoQueueSize()).toBe(2)
expect(await isModified()).toBe(false)
expect(await getUndoQueueSize()).toBe(0)
expect(await getRedoQueueSize()).toBe(2)
})
})
@@ -148,20 +174,4 @@ test.describe('Change Tracker', () => {
await expect(node).toBePinned()
await expect(node).toBeCollapsed()
})
test('Can detect changes in workflow.extra', async ({ comfyPage }) => {
expect(await comfyPage.getUndoQueueSize()).toBe(0)
await comfyPage.page.evaluate(() => {
window['app'].graph.extra.foo = 'bar'
})
// Click empty space to trigger a change detection.
await comfyPage.clickEmptySpace()
expect(await comfyPage.getUndoQueueSize()).toBe(1)
})
test('Ignores changes in workflow.ds', async ({ comfyPage }) => {
expect(await comfyPage.getUndoQueueSize()).toBe(0)
await comfyPage.pan({ x: 10, y: 10 })
expect(await comfyPage.getUndoQueueSize()).toBe(0)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -17,11 +17,8 @@ import {
import { Topbar } from './components/Topbar'
import { NodeReference } from './utils/litegraphUtils'
import type { Position, Size } from './types'
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
import { SettingDialog } from './components/SettingDialog'
type WorkspaceStore = ReturnType<typeof useWorkspaceStore>
class ComfyMenu {
public readonly sideToolbar: Locator
public readonly themeToggleButton: Locator
@@ -791,26 +788,6 @@ export class ComfyPage {
async moveMouseToEmptyArea() {
await this.page.mouse.move(10, 10)
}
async getUndoQueueSize() {
return this.page.evaluate(() => {
const workflow = (window['app'].extensionManager as WorkspaceStore)
.workflow.activeWorkflow
return workflow?.changeTracker.undoQueue.length
})
}
async getRedoQueueSize() {
return this.page.evaluate(() => {
const workflow = (window['app'].extensionManager as WorkspaceStore)
.workflow.activeWorkflow
return workflow?.changeTracker.redoQueue.length
})
}
async isCurrentWorkflowModified() {
return this.page.evaluate(() => {
return (window['app'].extensionManager as WorkspaceStore).workflow
.activeWorkflow?.isModified
})
}
}
export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({

View File

@@ -1,41 +0,0 @@
import { Page } from 'playwright'
import { test as base } from '@playwright/test'
export class UserSelectPage {
constructor(
public readonly url: string,
public readonly page: Page
) {}
get selectionUrl() {
return this.url + '/user-select'
}
get container() {
return this.page.locator('#comfy-user-selection')
}
get newUserInput() {
return this.container.locator('#new-user-input')
}
get existingUserSelect() {
return this.container.locator('#existing-user-select')
}
get nextButton() {
return this.container.getByText('Next')
}
}
export const userSelectPageFixture = base.extend<{
userSelectPage: UserSelectPage
}>({
userSelectPage: async ({ page }, use) => {
const userSelectPage = new UserSelectPage(
process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188',
page
)
await use(userSelectPage)
}
})

View File

@@ -103,36 +103,6 @@ test.describe('Group Node', () => {
await expect(comfyPage.page.locator('.node-tooltip')).toBeVisible()
})
test('Manage group opens with the correct group selected', async ({
comfyPage
}) => {
const makeGroup = async (name, type1, type2) => {
const node1 = (await comfyPage.getNodeRefsByType(type1))[0]
const node2 = (await comfyPage.getNodeRefsByType(type2))[0]
await node1.click('title')
await node2.click('title', {
modifiers: ['Shift']
})
return await node2.convertToGroupNode(name)
}
const group1 = await makeGroup(
'g1',
'CLIPTextEncode',
'CheckpointLoaderSimple'
)
const group2 = await makeGroup('g2', 'EmptyLatentImage', 'KSampler')
const manage1 = await group1.manageGroupNode()
await comfyPage.nextFrame()
expect(await manage1.getSelectedNodeType()).toBe('g1')
await manage1.close()
await expect(manage1.root).not.toBeVisible()
const manage2 = await group2.manageGroupNode()
expect(await manage2.getSelectedNodeType()).toBe('g2')
})
test('Reconnects inputs after configuration changed via manage dialog save', async ({
comfyPage
}) => {

View File

@@ -1,14 +1,12 @@
import { Locator, Page } from '@playwright/test'
export class ManageGroupNode {
footer: Locator
header: Locator
constructor(
readonly page: Page,
readonly root: Locator
) {
this.footer = root.locator('footer')
this.header = root.locator('header')
}
async setLabel(name: string, label: string) {
@@ -25,11 +23,6 @@ export class ManageGroupNode {
await this.footer.getByText('Close').click()
}
async getSelectedNodeType() {
const select = this.header.locator('select').first()
return await select.inputValue()
}
async selectNode(name: string) {
const list = this.root.locator('.comfy-group-manage-list-items')
const item = list.getByText(name)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

@@ -350,6 +350,23 @@ test.describe('Menu', () => {
await comfyPage.page.waitForTimeout(1000)
expect(await tab.getNode('KSampler (Advanced)').count()).toBe(2)
})
test('Can migrate legacy bookmarks', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks', [
'foo/',
'foo/KSampler (Advanced)',
'UNKNOWN',
'KSampler'
])
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [])
await comfyPage.reload()
expect(await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks')).toEqual(
[]
)
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual(['foo/', 'foo/KSamplerAdvanced', 'KSampler'])
})
})
test.describe('Workflows sidebar', () => {
@@ -448,20 +465,6 @@ test.describe('Menu', () => {
).toEqual(['workflow5.json'])
})
test('Can save temporary workflow with unmodified name', async ({
comfyPage
}) => {
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
await comfyPage.menu.topbar.saveWorkflow('Unsaved Workflow')
// Should not trigger the overwrite dialog
expect(
await comfyPage.page.locator('.comfy-modal-content:visible').count()
).toBe(0)
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
})
test('Can overwrite other workflows with save as', async ({
comfyPage
}) => {

View File

@@ -186,22 +186,4 @@ test.describe('Node Right Click Menu', () => {
'selected-nodes-unpinned.png'
)
})
test('Can clone pinned nodes', async ({ comfyPage }) => {
const nodeCount = await comfyPage.getGraphNodesCount()
const node = (await comfyPage.getFirstNodeRef())!
await node.clickContextMenuOption('Pin')
await comfyPage.nextFrame()
await node.click('title', { button: 'right' })
await expect(
comfyPage.page.locator('.litemenu-entry:has-text("Unpin")')
).toBeAttached()
const cloneItem = comfyPage.page.locator(
'.litemenu-entry:has-text("Clone")'
)
await cloneItem.click()
await expect(cloneItem).toHaveCount(0)
await comfyPage.nextFrame()
expect(await comfyPage.getGraphNodesCount()).toBe(nodeCount + 1)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -1,42 +0,0 @@
import { expect } from '@playwright/test'
import { userSelectPageFixture as test } from './fixtures/UserSelectPage'
/**
* Expects ComfyUI backend to be launched with `--multi-user` flag.
*/
test.describe('User Select View', () => {
test.beforeEach(async ({ userSelectPage, page }) => {
await page.goto(userSelectPage.url)
await page.evaluate(() => {
localStorage.clear()
sessionStorage.clear()
})
})
test('Redirects to user select view if no user is logged in', async ({
userSelectPage,
page
}) => {
await page.goto(userSelectPage.url)
await expect(userSelectPage.container).toBeVisible()
expect(page.url()).toBe(userSelectPage.selectionUrl)
})
test('Can create new user', async ({ userSelectPage, page }) => {
const randomUser = `test-user-${Math.random().toString(36).substring(2, 7)}`
await page.goto(userSelectPage.url)
await expect(page).toHaveURL(userSelectPage.selectionUrl)
await userSelectPage.newUserInput.fill(randomUser)
await userSelectPage.nextButton.click()
await expect(page).toHaveURL(userSelectPage.url)
})
test('Can choose existing user', async ({ userSelectPage, page }) => {
await page.goto(userSelectPage.url)
await expect(page).toHaveURL(userSelectPage.selectionUrl)
await userSelectPage.existingUserSelect.click()
await page.locator('.p-select-list .p-select-option').first().click()
await userSelectPage.nextButton.click()
await expect(page).toHaveURL(userSelectPage.url)
})
})

View File

@@ -9,6 +9,33 @@
</head>
<body class="litegraph grid">
<div id="vue-app"></div>
<div id="comfy-user-selection" class="comfy-user-selection" style="display: none;">
<main class="comfy-user-selection-inner">
<h1>ComfyUI</h1>
<form>
<section>
<label>New user:
<input placeholder="Enter a username" />
</label>
</section>
<div class="comfy-user-existing">
<span class="or-separator">OR</span>
<section>
<label>
Existing user:
<select>
<option hidden disabled selected value> Select a user </option>
</select>
</label>
</section>
</div>
<footer>
<span class="comfy-user-error">&nbsp;</span>
<button class="comfy-btn comfy-user-button-next">Next</button>
</footer>
</form>
</main>
</div>
<script type="module" src="src/main.ts"></script>
</body>
</html>

View File

@@ -3,16 +3,8 @@ export default {
'./**/*.{ts,tsx,vue}': (stagedFiles) => [
...formatFiles(stagedFiles),
'vue-tsc --noEmit',
'tsc --noEmit',
'tsc-strict'
],
'./src/locales/en.json': () => ['lobe-i18n locale'],
'./src/constants/coreSettings.ts': () => [
'tsx scripts/update-setting-locale.ts',
'lobe-i18n locale'
]
}

5012
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,17 @@
{
"name": "@comfyorg/comfyui-frontend",
"name": "comfyui-frontend",
"private": true,
"version": "1.5.4",
"version": "1.4.9",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
"description": "Official front-end implementation of ComfyUI",
"license": "GPL-3.0-only",
"scripts": {
"dev": "vite",
"dev:electron": "vite --config vite.electron.config.mts",
"build": "npm run typecheck && vite build",
"build:types": "vite build --config vite.types.config.mts && node scripts/prepare-types.js",
"deploy": "npm run build && node scripts/deploy.js",
"release": "node scripts/release.js",
"update-litegraph": "node scripts/update-litegraph.js",
"zipdist": "node scripts/zipdist.js",
"typecheck": "vue-tsc --noEmit && tsc --noEmit && tsc-strict",
"typecheck": "tsc --noEmit && tsc-strict",
"format": "prettier --write './**/*.{js,ts,tsx,vue}'",
"test": "jest --config jest.config.base.ts",
"test:jest:fast": "jest --config jest.config.fast.ts",
@@ -28,15 +23,13 @@
"prepare": "husky || true",
"preview": "vite preview",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"locale": "lobe-i18n locale"
"lint:fix": "eslint src --fix"
},
"devDependencies": {
"@babel/core": "^7.24.7",
"@babel/preset-env": "^7.22.20",
"@eslint/js": "^9.8.0",
"@iconify/json": "^2.2.245",
"@lobehub/i18n-cli": "^1.20.1",
"@pinia/testing": "^0.1.5",
"@playwright/test": "^1.44.1",
"@types/jest": "^29.5.12",
@@ -73,16 +66,14 @@
"unplugin-icons": "^0.19.3",
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.6",
"vite-plugin-dts": "^4.3.0",
"vite-plugin-static-copy": "^1.0.5",
"vitest": "^2.0.5",
"vue-tsc": "^2.1.10",
"zip-dir": "^2.0.0"
},
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.3.19",
"@comfyorg/litegraph": "^0.8.42",
"@comfyorg/comfyui-electron-types": "^0.3.6",
"@comfyorg/litegraph": "^0.8.35",
"@primevue/themes": "^4.0.5",
"@vueuse/core": "^11.0.0",
"@xterm/addon-fit": "^0.10.0",

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="292" height="92pt" viewBox="0 0 219 92"><defs><clipPath id="a"><path d="M159 .79h25V69h-25Zm0 0"/></clipPath><clipPath id="b"><path d="M183 9h35.371v60H183Zm0 0"/></clipPath><clipPath id="c"><path d="M0 .79h92V92H0Zm0 0"/></clipPath></defs><path style="stroke:none;fill-rule:nonzero;fill:#fff;fill-opacity:1" d="M130.871 31.836c-4.785 0-8.351 2.352-8.351 8.008 0 4.261 2.347 7.222 8.093 7.222 4.871 0 8.18-2.867 8.18-7.398 0-5.133-2.961-7.832-7.922-7.832Zm-9.57 39.95c-1.133 1.39-2.262 2.87-2.262 4.612 0 3.48 4.434 4.524 10.527 4.524 5.051 0 11.926-.352 11.926-5.043 0-2.793-3.308-2.965-7.488-3.227Zm25.761-39.688c1.563 2.004 3.22 4.789 3.22 8.793 0 9.656-7.571 15.316-18.536 15.316-2.789 0-5.312-.348-6.879-.785l-2.87 4.613 8.526.52c15.059.96 23.934 1.398 23.934 12.968 0 10.008-8.789 15.665-23.934 15.665-15.75 0-21.757-4.004-21.757-10.88 0-3.917 1.742-6 4.789-8.878-2.875-1.211-3.828-3.387-3.828-5.739 0-1.914.953-3.656 2.523-5.312 1.566-1.652 3.305-3.305 5.395-5.219-4.262-2.09-7.485-6.617-7.485-13.058 0-10.008 6.613-16.88 19.93-16.88 3.742 0 6.004.344 8.008.872h16.972v7.394l-8.007.61"/><g clip-path="url(#a)"><path style="stroke:none;fill-rule:nonzero;fill:#fff;fill-opacity:1" d="M170.379 16.281c-4.961 0-7.832-2.87-7.832-7.836 0-4.957 2.871-7.656 7.832-7.656 5.05 0 7.922 2.7 7.922 7.656 0 4.965-2.871 7.836-7.922 7.836Zm-11.227 52.305V61.71l4.438-.606c1.219-.175 1.394-.437 1.394-1.746V33.773c0-.953-.261-1.566-1.132-1.824l-4.7-1.656.957-7.047h18.016V59.36c0 1.399.086 1.57 1.395 1.746l4.437.606v6.875h-24.805"/></g><g clip-path="url(#b)"><path style="stroke:none;fill-rule:nonzero;fill:#fff;fill-opacity:1" d="M218.371 65.21c-3.742 1.825-9.223 3.481-14.187 3.481-10.356 0-14.27-4.175-14.27-14.015V31.879c0-.524 0-.871-.7-.871h-6.093v-7.746c7.664-.871 10.707-4.703 11.664-14.188h8.27v12.36c0 .609 0 .87.695.87h12.27v8.704h-12.965v20.797c0 5.136 1.218 7.136 5.918 7.136 2.437 0 4.96-.609 7.047-1.39l2.351 7.66"/></g><g clip-path="url(#c)"><path style="stroke:none;fill-rule:nonzero;fill:#fff;fill-opacity:1" d="M89.422 42.371 49.629 2.582a5.868 5.868 0 0 0-8.3 0l-8.263 8.262 10.48 10.484a6.965 6.965 0 0 1 7.173 1.668 6.98 6.98 0 0 1 1.656 7.215l10.102 10.105a6.963 6.963 0 0 1 7.214 1.657 6.976 6.976 0 0 1 0 9.875 6.98 6.98 0 0 1-9.879 0 6.987 6.987 0 0 1-1.519-7.594l-9.422-9.422v24.793a6.979 6.979 0 0 1 1.848 1.32 6.988 6.988 0 0 1 0 9.88c-2.73 2.726-7.153 2.726-9.875 0a6.98 6.98 0 0 1 0-9.88 6.893 6.893 0 0 1 2.285-1.523V34.398a6.893 6.893 0 0 1-2.285-1.523 6.988 6.988 0 0 1-1.508-7.637L29.004 14.902 1.719 42.187a5.868 5.868 0 0 0 0 8.301l39.793 39.793a5.868 5.868 0 0 0 8.3 0l39.61-39.605a5.873 5.873 0 0 0 0-8.305"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

View File

@@ -1,40 +0,0 @@
import fs from 'fs'
import path from 'path'
const mainPackage = JSON.parse(fs.readFileSync('./package.json', 'utf8'))
// Create the types-only package.json
const typesPackage = {
name: `${mainPackage.name}-types`,
version: mainPackage.version,
types: './index.d.ts',
files: ['index.d.ts'],
publishConfig: {
access: 'public'
},
repository: mainPackage.repository,
homepage: mainPackage.homepage,
description: `TypeScript definitions for ${mainPackage.name}`,
license: mainPackage.license,
dependencies: {
'@comfyorg/litegraph': mainPackage.dependencies['@comfyorg/litegraph']
},
peerDependencies: {
vue: mainPackage.dependencies.vue,
zod: mainPackage.dependencies.zod
}
}
// Ensure dist directory exists
const distDir = './dist'
if (!fs.existsSync(distDir)) {
fs.mkdirSync(distDir, { recursive: true })
}
// Write the new package.json to the dist directory
fs.writeFileSync(
path.join(distDir, 'package.json'),
JSON.stringify(typesPackage, null, 2)
)
console.log('Types package.json have been prepared in the dist directory')

View File

@@ -1,32 +0,0 @@
import fs from 'fs'
import { CORE_SETTINGS } from '../src/constants/coreSettings'
interface SettingLocale {
name: string
tooltip?: string
}
const extractLocaleStrings = (): Record<string, SettingLocale> => {
return Object.fromEntries(
CORE_SETTINGS.sort((a, b) => a.id.localeCompare(b.id)).map((setting) => [
// '.' is not allowed in JSON keys, so we replace it with '_'
setting.id.replace(/\./g, '_'),
{
name: setting.name,
tooltip: setting.tooltip
}
])
)
}
const main = () => {
const localeStrings = extractLocaleStrings()
const localePath = './src/locales/en.json'
const globalLocale = JSON.parse(fs.readFileSync(localePath, 'utf-8'))
fs.writeFileSync(
localePath,
JSON.stringify({ ...globalLocale, settingsDialog: localeStrings }, null, 2)
)
}
main()

View File

@@ -1,5 +1,5 @@
<template>
<div class="relative overflow-hidden h-full w-full bg-black" ref="rootEl">
<div class="relative h-full w-full bg-black" ref="rootEl">
<div class="p-terminal rounded-none h-full w-full p-2">
<div class="h-full terminal-host" ref="terminalEl"></div>
</div>

View File

@@ -13,7 +13,18 @@ const terminalCreated = (
{ terminal, useAutoSize }: ReturnType<typeof useTerminal>,
root: Ref<HTMLElement>
) => {
const terminalApi = electronAPI().Terminal
// TODO: use types from electron package
const terminalApi = electronAPI()['Terminal'] as {
onOutput(cb: (message: string) => void): () => void
resize(cols: number, rows: number): void
restore(): Promise<{
buffer: string[]
pos: { x: number; y: number }
size: { cols: number; rows: number }
}>
storePos(x: number, y: number): void
write(data: string): void
}
let offData: IDisposable
let offOutput: () => void

View File

@@ -1,7 +1,7 @@
<template>
<div class="grid grid-cols-2 gap-2">
<template v-for="col in deviceColumns" :key="col.field">
<div class="font-medium">{{ col.header }}</div>
<div class="font-medium">{{ $t(col.header) }}</div>
<div>{{ formatValue(props.device[col.field], col.field) }}</div>
</template>
</div>

View File

@@ -1,7 +1,7 @@
<!-- A generalized form item for rendering in a form. -->
<template>
<div class="form-label flex flex-grow items-center">
<span class="text-muted" :class="props.labelClass">
<span class="text-[var(--p-text-muted-color)]">
<slot name="name-prefix"></slot>
{{ props.item.name }}
<i
@@ -33,11 +33,15 @@ import CustomFormValue from '@/components/common/CustomFormValue.vue'
import InputSlider from '@/components/common/InputSlider.vue'
const formValue = defineModel<any>('formValue')
const props = defineProps<{
item: FormItem
id?: string
labelClass?: string | Record<string, boolean>
}>()
const props = withDefaults(
defineProps<{
item: FormItem
id: string | undefined
}>(),
{
id: undefined
}
)
function getFormAttrs(item: FormItem) {
const attrs = { ...(item.attrs || {}) }

View File

@@ -44,22 +44,21 @@ import Button from 'primevue/button'
import SearchFilterChip from './SearchFilterChip.vue'
import { toRefs } from 'vue'
const props = withDefaults(
defineProps<{
class?: string
modelValue: string
placeholder?: string
icon?: string
debounceTime?: number
filterIcon?: string
filters?: TFilter[]
}>(),
{
placeholder: 'Search...',
icon: 'pi pi-search',
debounceTime: 300
}
)
interface Props {
class?: string
modelValue: string
placeholder?: string
icon?: string
debounceTime?: number
filterIcon?: string
filters?: TFilter[]
}
const props = withDefaults(defineProps<Props>(), {
placeholder: 'Search...',
icon: 'pi pi-search',
debounceTime: 300
})
const { filters } = toRefs(props)

View File

@@ -4,7 +4,7 @@
<h2 class="text-2xl font-semibold mb-4">{{ $t('systemInfo') }}</h2>
<div class="grid grid-cols-2 gap-2">
<template v-for="col in systemColumns" :key="col.field">
<div class="font-medium">{{ col.header }}</div>
<div class="font-medium">{{ $t(col.header) }}</div>
<div>{{ formatValue(systemInfo[col.field], col.field) }}</div>
</template>
</div>

View File

@@ -1,51 +1,61 @@
<!-- The main global dialog to show various things -->
<template>
<Dialog
v-for="(item, index) in dialogStore.dialogStack"
:key="item.key"
v-model:visible="item.visible"
v-model:visible="dialogStore.isVisible"
class="global-dialog"
v-bind="item.dialogComponentProps"
:auto-z-index="false"
:pt:mask:style="{ zIndex: baseZIndex + index + 1 }"
:aria-labelledby="item.key"
modal
closable
closeOnEscape
dismissableMask
:maximizable="maximizable"
:maximized="maximized"
@hide="dialogStore.closeDialog"
@maximize="onMaximize"
@unmaximize="onUnmaximize"
:aria-labelledby="headerId"
>
<template #header>
<component
v-if="item.headerComponent"
:is="item.headerComponent"
:id="item.key"
v-if="dialogStore.headerComponent"
:is="dialogStore.headerComponent"
:id="headerId"
/>
<h3 v-else :id="item.key">{{ item.title || ' ' }}</h3>
<h3 v-else :id="headerId">{{ dialogStore.title || ' ' }}</h3>
</template>
<component
:is="item.component"
v-bind="item.contentProps"
:maximized="item.dialogComponentProps.maximized"
/>
<component :is="dialogStore.component" v-bind="contentProps" />
</Dialog>
</template>
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { ZIndex } from '@primeuix/utils/zindex'
import { usePrimeVue } from '@primevue/core'
import { computed, ref } from 'vue'
import { useDialogStore } from '@/stores/dialogStore'
import Dialog from 'primevue/dialog'
const dialogStore = useDialogStore()
const maximizable = computed(
() => dialogStore.dialogComponentProps.maximizable ?? false
)
const maximized = ref(false)
const primevue = usePrimeVue()
const onMaximize = () => {
maximized.value = true
}
const baseZIndex = computed(() => {
return primevue?.config?.zIndex?.modal ?? 1100
})
const onUnmaximize = () => {
maximized.value = false
}
onMounted(() => {
const mask = document.createElement('div')
ZIndex.set('model', mask, baseZIndex.value)
})
const contentProps = computed(() =>
maximizable.value
? {
...dialogStore.props,
maximized: maximized.value
}
: dialogStore.props
)
const headerId = `dialog-${Math.random().toString(36).substr(2, 9)}`
</script>
<style>

View File

@@ -38,6 +38,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useClipboard } from '@vueuse/core'
import { useToast } from 'primevue/usetoast'
import Button from 'primevue/button'
import Divider from 'primevue/divider'
@@ -49,7 +50,6 @@ import type { ExecutionErrorWsMessage, SystemStats } from '@/types/apiTypes'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { isElectron } from '@/utils/envUtil'
import { useCopyToClipboard } from '@/hooks/clipboardHooks'
const props = defineProps<{
error: ExecutionErrorWsMessage
@@ -65,6 +65,7 @@ const showReport = () => {
const showSendError = isElectron()
const toast = useToast()
const { copy, isSupported } = useClipboard()
onMounted(async () => {
try {
@@ -139,9 +140,30 @@ ${workflowText}
`
}
const { copyToClipboard } = useCopyToClipboard()
const copyReportToClipboard = async () => {
await copyToClipboard(reportContent.value)
if (isSupported) {
try {
await copy(reportContent.value)
toast.add({
severity: 'success',
summary: 'Success',
detail: 'Report copied to clipboard',
life: 3000
})
} catch (err) {
toast.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to copy report'
})
}
} else {
toast.add({
severity: 'error',
summary: 'Error',
detail: 'Clipboard API not supported in your browser'
})
}
}
const openNewGithubIssue = async () => {

View File

@@ -17,47 +17,71 @@
/>
</ScrollPanel>
<Divider layout="vertical" class="mx-1 2xl:mx-4" />
<Tabs :value="tabValue" :lazy="true" class="settings-content h-full w-full">
<TabPanels class="settings-tab-panels h-full w-full pr-0">
<PanelTemplate value="Search Results">
<SettingsPanel :settingGroups="searchResults" />
</PanelTemplate>
<PanelTemplate
v-for="category in settingCategories"
:key="category.key"
:value="category.label"
>
<template #header>
<CurrentUserMessage v-if="tabValue === 'Comfy'" />
<FirstTimeUIMessage v-if="tabValue === 'Comfy'" />
</template>
<SettingsPanel :settingGroups="sortedGroups(category)" />
</PanelTemplate>
<AboutPanel />
<Suspense>
<KeybindingPanel />
<template #fallback>
<div>Loading keybinding panel...</div>
</template>
</Suspense>
<Suspense>
<ExtensionPanel />
<template #fallback>
<div>Loading extension panel...</div>
</template>
</Suspense>
<Suspense>
<ServerConfigPanel />
<template #fallback>
<div>Loading server config panel...</div>
</template>
</Suspense>
</TabPanels>
</Tabs>
<ScrollPanel class="settings-content flex-grow">
<Tabs :value="tabValue" :lazy="true">
<FirstTimeUIMessage v-if="tabValue === 'Comfy'" />
<TabPanels class="settings-tab-panels">
<TabPanel key="search-results" value="Search Results">
<div v-if="searchResults.length > 0">
<SettingGroup
v-for="(group, i) in searchResults"
:key="group.label"
:divider="i !== 0"
:group="group"
/>
</div>
<NoResultsPlaceholder
v-else
icon="pi pi-search"
:title="$t('noResultsFound')"
:message="$t('searchFailedMessage')"
/>
</TabPanel>
<TabPanel
v-for="category in categories"
:key="category.key"
:value="category.label"
>
<SettingGroup
v-for="(group, i) in sortedGroups(category)"
:key="group.label"
:divider="i !== 0"
:group="{
label: group.label,
settings: flattenTree<SettingParams>(group)
}"
/>
</TabPanel>
<TabPanel key="about" value="About">
<AboutPanel />
</TabPanel>
<TabPanel key="keybinding" value="Keybinding">
<Suspense>
<KeybindingPanel />
<template #fallback>
<div>Loading keybinding panel...</div>
</template>
</Suspense>
</TabPanel>
<TabPanel key="extension" value="Extension">
<Suspense>
<ExtensionPanel />
<template #fallback>
<div>Loading extension panel...</div>
</template>
</Suspense>
</TabPanel>
<TabPanel key="server-config" value="Server-Config">
<Suspense>
<ServerConfigPanel />
<template #fallback>
<div>Loading server config panel...</div>
</template>
</Suspense>
</TabPanel>
</TabPanels>
</Tabs>
</ScrollPanel>
</div>
</template>
@@ -66,17 +90,17 @@ import { ref, computed, onMounted, watch, defineAsyncComponent } from 'vue'
import Listbox from 'primevue/listbox'
import Tabs from 'primevue/tabs'
import TabPanels from 'primevue/tabpanels'
import TabPanel from 'primevue/tabpanel'
import Divider from 'primevue/divider'
import ScrollPanel from 'primevue/scrollpanel'
import { SettingTreeNode, useSettingStore } from '@/stores/settingStore'
import { ISettingGroup, SettingParams } from '@/types/settingTypes'
import { SettingParams } from '@/types/settingTypes'
import SettingGroup from './setting/SettingGroup.vue'
import SearchBox from '@/components/common/SearchBox.vue'
import SettingsPanel from './setting/SettingsPanel.vue'
import PanelTemplate from './setting/PanelTemplate.vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import { flattenTree } from '@/utils/treeUtil'
import AboutPanel from './setting/AboutPanel.vue'
import FirstTimeUIMessage from './setting/FirstTimeUIMessage.vue'
import CurrentUserMessage from './setting/CurrentUserMessage.vue'
import { flattenTree } from '@/utils/treeUtil'
import { isElectron } from '@/utils/envUtil'
const KeybindingPanel = defineAsyncComponent(
@@ -89,6 +113,11 @@ const ServerConfigPanel = defineAsyncComponent(
() => import('./setting/ServerConfigPanel.vue')
)
interface ISettingGroup {
label: string
settings: SettingParams[]
}
const aboutPanelNode: SettingTreeNode = {
key: 'about',
label: 'About',
@@ -129,11 +158,8 @@ const serverConfigPanelNodeList = computed<SettingTreeNode[]>(() => {
const settingStore = useSettingStore()
const settingRoot = computed<SettingTreeNode>(() => settingStore.settingTree)
const settingCategories = computed<SettingTreeNode[]>(
() => settingRoot.value.children ?? []
)
const categories = computed<SettingTreeNode[]>(() => [
...settingCategories.value,
...(settingRoot.value.children || []),
keybindingPanelNode,
...extensionPanelNodeList.value,
...serverConfigPanelNodeList.value,
@@ -152,13 +178,10 @@ onMounted(() => {
activeCategory.value = categories.value[0]
})
const sortedGroups = (category: SettingTreeNode): ISettingGroup[] => {
return [...(category.children ?? [])]
.sort((a, b) => a.label.localeCompare(b.label))
.map((group) => ({
label: group.label,
settings: flattenTree<SettingParams>(group)
}))
const sortedGroups = (category: SettingTreeNode) => {
return [...(category.children || [])].sort((a, b) =>
a.label.localeCompare(b.label)
)
}
const searchQuery = ref<string>('')

View File

@@ -1,5 +1,5 @@
<template>
<PanelTemplate value="About" class="about-container">
<div class="about-container">
<h2 class="text-2xl font-bold mb-2">{{ $t('about') }}</h2>
<div class="space-y-2">
<a
@@ -26,11 +26,10 @@
v-if="systemStatsStore.systemStats"
:stats="systemStatsStore.systemStats"
/>
</PanelTemplate>
</div>
</template>
<script setup lang="ts">
import PanelTemplate from './PanelTemplate.vue'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import { useAboutPanelStore } from '@/stores/aboutPanelStore'
import Tag from 'primevue/tag'

View File

@@ -1,26 +0,0 @@
<!-- A message that displays the current user -->
<template>
<Message
v-if="userStore.isMultiUserServer"
severity="info"
icon="pi pi-user"
pt:text="w-full"
>
<div class="flex items-center justify-between">
<div>{{ $t('currentUser') }}: {{ userStore.currentUser?.username }}</div>
<Button icon="pi pi-sign-out" @click="logout" text />
</div>
</Message>
</template>
<script setup lang="ts">
import Message from 'primevue/message'
import Button from 'primevue/button'
import { useUserStore } from '@/stores/userStore'
const userStore = useUserStore()
const logout = () => {
userStore.logout()
window.location.reload()
}
</script>

View File

@@ -1,35 +1,6 @@
<template>
<PanelTemplate value="Extension" class="extension-panel">
<template #header>
<SearchBox
v-model="filters['global'].value"
:placeholder="$t('searchExtensions') + '...'"
/>
<Message v-if="hasChanges" severity="info" pt:text="w-full">
<ul>
<li v-for="ext in changedExtensions" :key="ext.name">
<span>
{{ extensionStore.isExtensionEnabled(ext.name) ? '[-]' : '[+]' }}
</span>
{{ ext.name }}
</li>
</ul>
<div class="flex justify-end">
<Button
:label="$t('reloadToApplyChanges')"
@click="applyChanges"
outlined
severity="danger"
/>
</div>
</Message>
</template>
<DataTable
:value="extensionStore.extensions"
stripedRows
size="small"
:filters="filters"
>
<div class="extension-panel">
<DataTable :value="extensionStore.extensions" stripedRows size="small">
<Column field="name" :header="$t('extensionName')" sortable></Column>
<Column
:pt="{
@@ -44,7 +15,28 @@
</template>
</Column>
</DataTable>
</PanelTemplate>
<div class="mt-4">
<Message v-if="hasChanges" severity="info">
<ul>
<li v-for="ext in changedExtensions" :key="ext.name">
<span>
{{ extensionStore.isExtensionEnabled(ext.name) ? '[-]' : '[+]' }}
</span>
{{ ext.name }}
</li>
</ul>
</Message>
<Button
:label="$t('reloadToApplyChanges')"
icon="pi pi-refresh"
@click="applyChanges"
:disabled="!hasChanges"
text
fluid
severity="danger"
/>
</div>
</div>
</template>
<script setup lang="ts">
@@ -56,13 +48,6 @@ import Column from 'primevue/column'
import ToggleSwitch from 'primevue/toggleswitch'
import Button from 'primevue/button'
import Message from 'primevue/message'
import { FilterMatchMode } from '@primevue/core/api'
import PanelTemplate from './PanelTemplate.vue'
import SearchBox from '@/components/common/SearchBox.vue'
const filters = ref({
global: { value: '', matchMode: FilterMatchMode.CONTAINS }
})
const extensionStore = useExtensionStore()
const settingStore = useSettingStore()

View File

@@ -1,7 +1,7 @@
<template>
<Message
v-if="show"
class="first-time-ui-message"
class="first-time-ui-message m-2"
severity="info"
:closable="true"
@close="handleClose"

View File

@@ -1,12 +1,5 @@
<template>
<PanelTemplate value="Keybinding" class="keybinding-panel">
<template #header>
<SearchBox
v-model="filters['global'].value"
:placeholder="$t('searchKeybindings') + '...'"
/>
</template>
<div class="keybinding-panel">
<DataTable
:value="commandsData"
v-model:selection="selectedCommandData"
@@ -18,6 +11,12 @@
header: 'px-0'
}"
>
<template #header>
<SearchBox
v-model="filters['global'].value"
:placeholder="$t('searchKeybindings') + '...'"
/>
</template>
<Column field="actions" header="">
<template #body="slotProps">
<div class="actions invisible flex flex-row">
@@ -110,7 +109,7 @@
text
@click="resetKeybindings"
/>
</PanelTemplate>
</div>
</template>
<script setup lang="ts">
@@ -128,7 +127,6 @@ import Dialog from 'primevue/dialog'
import InputText from 'primevue/inputtext'
import Message from 'primevue/message'
import Tag from 'primevue/tag'
import PanelTemplate from './PanelTemplate.vue'
import KeyComboDisplay from './keybinding/KeyComboDisplay.vue'
import SearchBox from '@/components/common/SearchBox.vue'
import { useToast } from 'primevue/usetoast'

View File

@@ -1,21 +0,0 @@
<template>
<TabPanel :value="props.value" class="h-full w-full" :class="props.class">
<div class="flex flex-col h-full w-full gap-2">
<slot name="header" />
<ScrollPanel class="flex-grow h-0 pr-2">
<slot />
</ScrollPanel>
<slot name="footer" />
</div>
</TabPanel>
</template>
<script setup lang="ts">
import TabPanel from 'primevue/tabpanel'
import ScrollPanel from 'primevue/scrollpanel'
const props = defineProps<{
value: string
class?: string
}>()
</script>

View File

@@ -1,105 +1,37 @@
<template>
<PanelTemplate value="Server-Config" class="server-config-panel">
<template #header>
<div class="flex flex-col gap-2">
<Message
v-if="modifiedConfigs.length > 0"
severity="info"
pt:text="w-full"
>
<p>
{{ $t('serverConfig.modifiedConfigs') }}
</p>
<ul>
<li v-for="config in modifiedConfigs" :key="config.id">
{{ config.name }}: {{ config.initialValue }} {{ config.value }}
</li>
</ul>
<div class="flex justify-end gap-2">
<Button
:label="$t('serverConfig.revertChanges')"
@click="revertChanges"
outlined
/>
<Button
:label="$t('serverConfig.restart')"
@click="restartApp"
outlined
severity="danger"
/>
</div>
</Message>
<Message v-if="commandLineArgs" severity="secondary" pt:text="w-full">
<template #icon>
<i-lucide:terminal class="text-xl font-bold" />
</template>
<div class="flex items-center justify-between">
<p>{{ commandLineArgs }}</p>
<Button
icon="pi pi-clipboard"
@click="copyCommandLineArgs"
severity="secondary"
text
/>
</div>
</Message>
</div>
</template>
<div
v-for="([label, items], i) in Object.entries(serverConfigsByCategory)"
:key="label"
>
<Divider v-if="i > 0" />
<h3>{{ formatCamelCase(label) }}</h3>
<div
v-for="item in items"
:key="item.name"
class="flex items-center mb-4"
>
<FormItem
:item="item"
v-model:formValue="item.value"
:id="item.id"
:labelClass="{
'text-highlight': item.initialValue !== item.value
}"
/>
</div>
<div
v-for="([label, items], i) in Object.entries(serverConfigsByCategory)"
:key="label"
>
<Divider v-if="i > 0" />
<h3>{{ formatCamelCase(label) }}</h3>
<div v-for="item in items" :key="item.name" class="flex items-center mb-4">
<FormItem :item="item" v-model:formValue="item.value" />
</div>
</PanelTemplate>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Message from 'primevue/message'
import Divider from 'primevue/divider'
import FormItem from '@/components/common/FormItem.vue'
import PanelTemplate from './PanelTemplate.vue'
import { formatCamelCase } from '@/utils/formatUtil'
import { useSettingStore } from '@/stores/settingStore'
import { useServerConfigStore } from '@/stores/serverConfigStore'
import { storeToRefs } from 'pinia'
import { electronAPI } from '@/utils/envUtil'
import { useSettingStore } from '@/stores/settingStore'
import { watch } from 'vue'
import { useCopyToClipboard } from '@/hooks/clipboardHooks'
import { onMounted, watch } from 'vue'
import { SERVER_CONFIG_ITEMS } from '@/constants/serverConfig'
const settingStore = useSettingStore()
const serverConfigStore = useServerConfigStore()
const {
serverConfigsByCategory,
serverConfigValues,
launchArgs,
commandLineArgs,
modifiedConfigs
} = storeToRefs(serverConfigStore)
const { serverConfigsByCategory, launchArgs, serverConfigValues } =
storeToRefs(serverConfigStore)
const revertChanges = () => {
serverConfigStore.revertChanges()
}
const restartApp = () => {
electronAPI().restartApp()
}
onMounted(() => {
serverConfigStore.loadServerConfig(
SERVER_CONFIG_ITEMS,
settingStore.get('Comfy.Server.ServerConfigValues')
)
})
watch(launchArgs, (newVal) => {
settingStore.set('Comfy.Server.LaunchArgs', newVal)
@@ -108,9 +40,4 @@ watch(launchArgs, (newVal) => {
watch(serverConfigValues, (newVal) => {
settingStore.set('Comfy.Server.ServerConfigValues', newVal)
})
const { copyToClipboard } = useCopyToClipboard()
const copyCommandLineArgs = async () => {
await copyToClipboard(commandLineArgs.value)
}
</script>

View File

@@ -1,6 +1,6 @@
<template>
<FormItem
:item="formItem"
:item="setting"
:id="setting.id"
:formValue="settingValue"
@update:formValue="updateSettingValue"
@@ -22,24 +22,11 @@ import FormItem from '@/components/common/FormItem.vue'
import { useSettingStore } from '@/stores/settingStore'
import { SettingParams } from '@/types/settingTypes'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const props = defineProps<{
setting: SettingParams
}>()
const { t } = useI18n()
const formItem = computed(() => {
const normalizedId = props.setting.id.replace(/\./g, '_')
return {
...props.setting,
name: t(`settingsDialog.${normalizedId}.name`, props.setting.name),
tooltip: props.setting.tooltip
? t(`settingsDialog.${normalizedId}.tooltip`, props.setting.tooltip)
: undefined
}
})
const settingStore = useSettingStore()
const settingValue = computed(() => settingStore.get(props.setting.id))
const updateSettingValue = (value: any) => {

View File

@@ -1,26 +0,0 @@
<template>
<div v-if="props.settingGroups.length > 0">
<SettingGroup
v-for="(group, i) in props.settingGroups"
:key="group.label"
:divider="i !== 0"
:group="group"
/>
</div>
<NoResultsPlaceholder
v-else
icon="pi pi-search"
:title="$t('noResultsFound')"
:message="$t('searchFailedMessage')"
/>
</template>
<script setup lang="ts">
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import SettingGroup from './SettingGroup.vue'
import { ISettingGroup } from '@/types/settingTypes'
const props = defineProps<{
settingGroups: ISettingGroup[]
}>()
</script>

View File

@@ -163,12 +163,6 @@ watchEffect(() => {
}
})
watchEffect(() => {
const maximumFps = settingStore.get('LiteGraph.Canvas.MaximumFps')
const { canvas } = canvasStore
if (canvas) canvas.maximumFps = maximumFps
})
watchEffect(() => {
CanvasPointer.doubleClickTime = settingStore.get(
'Comfy.Pointer.DoubleClickTime'

View File

@@ -16,7 +16,7 @@
v-model="installPath"
class="w-full"
:class="{ 'p-invalid': pathError }"
@update:modelValue="validatePath"
@change="validatePath"
/>
<InputIcon
class="pi pi-info-circle"

View File

@@ -16,7 +16,7 @@
placeholder="Select existing ComfyUI installation (optional)"
class="flex-1"
:class="{ 'p-invalid': pathError }"
@update:modelValue="validateSource"
@change="validateSource"
/>
<Button icon="pi pi-folder" @click="browsePath" class="w-12" />
</div>
@@ -57,18 +57,6 @@
</p>
</div>
</div>
<div class="flex items-center gap-3 p-2 rounded cursor-not-allowed">
<Checkbox disabled :binary="true" />
<div>
<label class="text-neutral-200 font-medium">
{{ $t('install.customNodes') }}
<Tag severity="secondary"> {{ $t('comingSoon') }}... </Tag>
</label>
<p class="text-sm text-neutral-400 my-1">
{{ $t('install.customNodesDescription') }}
</p>
</div>
</div>
</div>
</div>
@@ -82,7 +70,6 @@
<script setup lang="ts">
import { ref, computed, watchEffect } from 'vue'
import { electronAPI } from '@/utils/envUtil'
import Tag from 'primevue/tag'
import InputText from 'primevue/inputtext'
import Button from 'primevue/button'
import Checkbox from 'primevue/checkbox'

View File

@@ -22,7 +22,7 @@
>
<template #container>
<NodeSearchBox
:filters="nodeFilters as FilterAndValue[]"
:filters="nodeFilters"
@add-filter="addFilter"
@remove-filter="removeFilter"
@add-node="addNode"
@@ -56,7 +56,7 @@ const getNewNodeLocation = (): [number, number] => {
if (triggerEvent.value === null) {
return [100, 100]
}
// @ts-expect-error type event detail
const originalEvent = triggerEvent.value.detail.originalEvent
return [originalEvent.canvasX, originalEvent.canvasY]
}
@@ -99,7 +99,6 @@ const newSearchBoxEnabled = computed(
)
const showSearchBox = (e: LiteGraphCanvasEvent) => {
if (newSearchBoxEnabled.value) {
// @ts-expect-error type event detail
if (e.detail.originalEvent?.pointerType === 'touch') {
setTimeout(() => {
showNewSearchBox(e)
@@ -108,16 +107,13 @@ const showSearchBox = (e: LiteGraphCanvasEvent) => {
showNewSearchBox(e)
}
} else {
// @ts-expect-error type event detail
canvasStore.canvas.showSearchBox(e.detail.originalEvent as MouseEvent)
}
}
const nodeDefStore = useNodeDefStore()
const showNewSearchBox = (e: LiteGraphCanvasEvent) => {
// @ts-expect-error type event detail
if (e.detail.linkReleaseContext) {
// @ts-expect-error type event detail
const links = e.detail.linkReleaseContext.links
if (links.length === 0) {
console.warn('Empty release with no links! This should never happen')
@@ -127,7 +123,7 @@ const showNewSearchBox = (e: LiteGraphCanvasEvent) => {
const filter = nodeDefStore.nodeSearchService.getFilterById(
firstLink.releaseSlotType
)
const dataType = firstLink.type.toString()
const dataType = firstLink.type
addFilter([filter, dataType])
}
@@ -142,7 +138,6 @@ const showNewSearchBox = (e: LiteGraphCanvasEvent) => {
}
const showContextMenu = (e: LiteGraphCanvasEvent) => {
// @ts-expect-error type event detail
const links = e.detail.linkReleaseContext.links
if (links.length === 0) {
console.warn('Empty release with no links! This should never happen')
@@ -150,7 +145,6 @@ const showContextMenu = (e: LiteGraphCanvasEvent) => {
}
const firstLink = ConnectingLinkImpl.createFromPlainObject(links[0])
// @ts-expect-error type event detail
const mouseEvent = e.detail.originalEvent as MouseEvent
const commonOptions = {
e: mouseEvent,
@@ -168,7 +162,6 @@ const showContextMenu = (e: LiteGraphCanvasEvent) => {
slotTo: firstLink.input,
afterRerouteId: firstLink.afterRerouteId
}
// @ts-expect-error type arguments
canvasStore.canvas.showConnectionMenu({
...connectionOptions,
...commonOptions
@@ -209,7 +202,6 @@ const linkReleaseActionShift = computed(() => {
})
const handleCanvasEmptyRelease = (e: LiteGraphCanvasEvent) => {
// @ts-expect-error type event detail
const originalEvent = e.detail.originalEvent as MouseEvent
const shiftPressed = originalEvent.shiftKey

View File

@@ -12,7 +12,6 @@
@click="onTabClick(tab)"
/>
<div class="side-tool-bar-end">
<SidebarLogoutIcon v-if="userStore.isMultiUserServer" />
<SidebarThemeToggleIcon />
<SidebarSettingsToggleIcon />
</div>
@@ -30,18 +29,15 @@
import SidebarIcon from './SidebarIcon.vue'
import SidebarThemeToggleIcon from './SidebarThemeToggleIcon.vue'
import SidebarSettingsToggleIcon from './SidebarSettingsToggleIcon.vue'
import SidebarLogoutIcon from './SidebarLogoutIcon.vue'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import { computed } from 'vue'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { useSettingStore } from '@/stores/settingStore'
import type { SidebarTabExtension } from '@/types/extensionTypes'
import { useKeybindingStore } from '@/stores/keybindingStore'
import { useUserStore } from '@/stores/userStore'
const workspaceStore = useWorkspaceStore()
const settingStore = useSettingStore()
const userStore = useUserStore()
const teleportTarget = computed(() =>
settingStore.get('Comfy.Sidebar.Location') === 'left'

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