mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-16 10:27:32 +00:00
Compare commits
2 Commits
sidebar-do
...
fix-queue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02c13c403f | ||
|
|
8254e3c9cf |
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
const nodeDef = {
|
||||
title: 'TestNodeAdvancedDoc'
|
||||
}
|
||||
|
||||
test.describe('Documentation Sidebar', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('default')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
const currentThemeId = await comfyPage.menu.getThemeId()
|
||||
if (currentThemeId !== 'dark') {
|
||||
await comfyPage.menu.toggleTheme()
|
||||
}
|
||||
})
|
||||
|
||||
test('Sidebar registered', async ({ comfyPage }) => {
|
||||
await expect(
|
||||
comfyPage.page.locator('.documentation-tab-button')
|
||||
).toBeVisible()
|
||||
})
|
||||
test('Parses help for basic node', async ({ comfyPage }) => {
|
||||
await comfyPage.page.locator('.documentation-tab-button').click()
|
||||
const docPane = comfyPage.page.locator('.sidebar-content-container')
|
||||
//Check that each independently parsed element exists
|
||||
await expect(docPane).toContainText('Load Checkpoint')
|
||||
await expect(docPane).toContainText('Loads a diffusion model')
|
||||
await expect(docPane).toContainText('The name of the checkpoint')
|
||||
await expect(docPane).toContainText('The VAE model used')
|
||||
})
|
||||
test('Responds to hovering over node', async ({ comfyPage }) => {
|
||||
await comfyPage.page.locator('.documentation-tab-button').click()
|
||||
const docPane = comfyPage.page.locator('.sidebar-content-container')
|
||||
await comfyPage.page.mouse.move(321, 593)
|
||||
const tooltipTimeout = 500
|
||||
await comfyPage.page.waitForTimeout(tooltipTimeout + 16)
|
||||
await expect(comfyPage.page.locator('.node-tooltip')).not.toBeVisible()
|
||||
await expect(
|
||||
comfyPage.page.locator('.sidebar-content-container>div>div:nth-child(4)')
|
||||
).toBeFocused()
|
||||
})
|
||||
test('Updates when a new node is selected', async ({ comfyPage }) => {
|
||||
await comfyPage.page.locator('.documentation-tab-button').click()
|
||||
const docPane = comfyPage.page.locator('.sidebar-content-container')
|
||||
await comfyPage.page.mouse.click(557, 440)
|
||||
await expect(docPane).not.toContainText('Load Checkpoint')
|
||||
await expect(docPane).toContainText('CLIP Text Encode (Prompt)')
|
||||
await expect(docPane).toContainText('The text to be encoded')
|
||||
await expect(docPane).toContainText(
|
||||
'A conditioning containing the embedded text'
|
||||
)
|
||||
})
|
||||
test('Responds to a change in theme', async ({ comfyPage }) => {
|
||||
await comfyPage.page.locator('.documentation-tab-button').click()
|
||||
const docPane = comfyPage.page.locator('.sidebar-content-container')
|
||||
await comfyPage.menu.toggleTheme()
|
||||
await expect(docPane).toHaveScreenshot(
|
||||
'documentation-sidebar-light-theme.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
test.describe('Advanced Description tests', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
//register test node and add to graph
|
||||
await comfyPage.page.evaluate(async (node) => {
|
||||
const app = window['app']
|
||||
await app.registerNodeDef(node.name, node)
|
||||
app.addNodeOnGraph(node)
|
||||
}, advDocNode)
|
||||
})
|
||||
test('Description displays as raw html', async ({ comfyPage }) => {
|
||||
await comfyPage.page.locator('.documentation-tab-button').click()
|
||||
const docPane = comfyPage.page.locator('.sidebar-content-container>div')
|
||||
await expect(docPane).toHaveJSProperty(
|
||||
'innerHTML',
|
||||
advDocNode.description[1]
|
||||
)
|
||||
})
|
||||
test('selected function', async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const app = window['app']
|
||||
const desc =
|
||||
LiteGraph.registered_node_types['Test_AdvancedDescription'].nodeData
|
||||
.description
|
||||
desc[2].select = (element, name, value) => {
|
||||
element.children[0].innerText = name + ' ' + value
|
||||
}
|
||||
})
|
||||
await comfyPage.page.locator('.documentation-tab-button').click()
|
||||
const docPane = comfyPage.page.locator('.sidebar-content-container')
|
||||
await comfyPage.page.mouse.move(307, 80)
|
||||
const tooltipTimeout = 500
|
||||
await comfyPage.page.waitForTimeout(tooltipTimeout + 16)
|
||||
await expect(comfyPage.page.locator('.node-tooltip')).not.toBeVisible()
|
||||
await expect(docPane).toContainText('int_input 0')
|
||||
})
|
||||
})
|
||||
|
||||
const advDocNode = {
|
||||
display_name: 'Node With Advanced Description',
|
||||
name: 'Test_AdvancedDescription',
|
||||
input: {
|
||||
required: {
|
||||
int_input: [
|
||||
'INT',
|
||||
{ tooltip: "an input tooltip that won't be displayed in sidebar" }
|
||||
]
|
||||
}
|
||||
},
|
||||
output: ['INT'],
|
||||
output_name: ['int_output'],
|
||||
output_tooltips: ["An output tooltip that won't be displayed in the sidebar"],
|
||||
output_is_list: false,
|
||||
description: [
|
||||
'A node with description in the advanced format',
|
||||
`
|
||||
A long form description that will be displayed in the sidebar.
|
||||
<div doc_title="INT">Can include arbitrary html</div>
|
||||
<div doc_title="int_input">or out of order widgets</div>
|
||||
`,
|
||||
{}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB |
@@ -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 }>({
|
||||
|
||||
@@ -465,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
|
||||
}) => {
|
||||
|
||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "comfyui-frontend",
|
||||
"version": "1.4.13",
|
||||
"version": "1.4.9",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "comfyui-frontend",
|
||||
"version": "1.4.13",
|
||||
"version": "1.4.9",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.3.19",
|
||||
"@comfyorg/litegraph": "^0.8.37",
|
||||
"@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",
|
||||
@@ -1917,15 +1917,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@comfyorg/comfyui-electron-types": {
|
||||
"version": "0.3.19",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.3.19.tgz",
|
||||
"integrity": "sha512-VNr542eaLVmaeSJIvGv/Y5OiC2vYiLy+FtjwYtP+J8M5BSy88GCikX2K9NIkLPtcw9DLolVK3XWuIvFpsOK0zg==",
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.3.6.tgz",
|
||||
"integrity": "sha512-wgMgESnCcRzvVkk8CwWiTAUJxC4LBvw5uTENxzaWkEL0qrnmiGrVLore00yX3cYz04hJaTA6PqasLqgVLDOenw==",
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.8.37",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.37.tgz",
|
||||
"integrity": "sha512-HI3msNigdlW1pz5HMU7+5UpLX0TkWkLD8qOeVBFTwq4tGjsEfqWs6lowyjsWSJcjef/0fVvjsKV5hsTbeVWVkA==",
|
||||
"version": "0.8.35",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.35.tgz",
|
||||
"integrity": "sha512-taxjPoNJLajZa3z3JSxwgArRIi5lYy3nlkmemup8bo0AtC7QpKOOE+xQ5wtSXcSMZZMxbsgQHp7FoBTeIUHngA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.4.13",
|
||||
"version": "1.4.9",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -72,8 +72,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.3.19",
|
||||
"@comfyorg/litegraph": "^0.8.37",
|
||||
"@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",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 174 KiB |
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 || {}) }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -17,46 +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>
|
||||
<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>
|
||||
|
||||
@@ -65,16 +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 { flattenTree } from '@/utils/treeUtil'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
const KeybindingPanel = defineAsyncComponent(
|
||||
@@ -87,6 +113,11 @@ const ServerConfigPanel = defineAsyncComponent(
|
||||
() => import('./setting/ServerConfigPanel.vue')
|
||||
)
|
||||
|
||||
interface ISettingGroup {
|
||||
label: string
|
||||
settings: SettingParams[]
|
||||
}
|
||||
|
||||
const aboutPanelNode: SettingTreeNode = {
|
||||
key: 'about',
|
||||
label: 'About',
|
||||
@@ -127,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,
|
||||
@@ -150,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>('')
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -10,23 +10,20 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref, watch } from 'vue'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useHoveredItemStore } from '@/stores/graphStore'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
|
||||
let idleTimeout: number
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const hoveredItemStore = useHoveredItemStore()
|
||||
const tooltipRef = ref<HTMLDivElement>()
|
||||
const tooltipText = ref('')
|
||||
const left = ref<string>()
|
||||
const top = ref<string>()
|
||||
|
||||
const hideTooltip = () => (tooltipText.value = null)
|
||||
const clearHovered = () => (hoveredItemStore.value = null)
|
||||
|
||||
const showTooltip = async (tooltip: string | null | undefined) => {
|
||||
if (!tooltip) return
|
||||
@@ -46,36 +43,6 @@ const showTooltip = async (tooltip: string | null | undefined) => {
|
||||
top.value = comfyApp.canvas.mouse[1] + rect.height + 'px'
|
||||
}
|
||||
}
|
||||
watch(hoveredItemStore, (hoveredItem) => {
|
||||
if (!hoveredItem.value) {
|
||||
return hideTooltip()
|
||||
}
|
||||
const item = hoveredItem.value
|
||||
const nodeDef =
|
||||
nodeDefStore.nodeDefsByName[item.node.type] ??
|
||||
LiteGraph.registered_node_types[item.node.type]?.nodeData
|
||||
if (item.type == 'Title') {
|
||||
let description = nodeDef.description
|
||||
if (Array.isArray(description)) {
|
||||
description = description[0]
|
||||
}
|
||||
return showTooltip(description)
|
||||
} else if (item.type == 'Input') {
|
||||
showTooltip(nodeDef.input.getInput(item.inputName)?.tooltip)
|
||||
} else if (item.type == 'Output') {
|
||||
showTooltip(nodeDef?.output?.all?.[item.outputSlot]?.tooltip)
|
||||
} else if (item.type == 'Widget') {
|
||||
showTooltip(
|
||||
item.widget.tooltip ??
|
||||
(
|
||||
nodeDef.input.optional?.[item.widget.name] ??
|
||||
nodeDef.input.required?.[item.widget.name]
|
||||
)?.tooltip
|
||||
)
|
||||
} else {
|
||||
hideTooltip()
|
||||
}
|
||||
})
|
||||
|
||||
const onIdle = () => {
|
||||
const { canvas } = comfyApp
|
||||
@@ -83,15 +50,13 @@ const onIdle = () => {
|
||||
if (!node) return
|
||||
|
||||
const ctor = node.constructor as { title_mode?: 0 | 1 | 2 | 3 }
|
||||
const nodeDef =
|
||||
nodeDefStore.nodeDefsByName[node.type] ??
|
||||
LiteGraph.registered_node_types[node.type]?.nodeData
|
||||
const nodeDef = nodeDefStore.nodeDefsByName[node.type]
|
||||
|
||||
if (
|
||||
ctor.title_mode !== LiteGraph.NO_TITLE &&
|
||||
canvas.graph_mouse[1] < node.pos[1] // If we are over a node, but not within the node then we are on its title
|
||||
) {
|
||||
hoveredItemStore.value = { node, type: 'Title' }
|
||||
return showTooltip(nodeDef.description)
|
||||
}
|
||||
|
||||
if (node.flags?.collapsed) return
|
||||
@@ -104,7 +69,7 @@ const onIdle = () => {
|
||||
)
|
||||
if (inputSlot !== -1) {
|
||||
const inputName = node.inputs[inputSlot].name
|
||||
hoveredItemStore.value = { node, type: 'Input', inputName }
|
||||
return showTooltip(nodeDef.input.getInput(inputName)?.tooltip)
|
||||
}
|
||||
|
||||
const outputSlot = canvas.isOverNodeOutput(
|
||||
@@ -114,18 +79,20 @@ const onIdle = () => {
|
||||
[0, 0]
|
||||
)
|
||||
if (outputSlot !== -1) {
|
||||
hoveredItemStore.value = { node, type: 'Output', outputSlot }
|
||||
return showTooltip(nodeDef.output.all?.[outputSlot]?.tooltip)
|
||||
}
|
||||
|
||||
const widget = comfyApp.canvas.getWidgetAtCursor()
|
||||
// Dont show for DOM widgets, these use native browser tooltips as we dont get proper mouse events on these
|
||||
if (widget && !widget.element) {
|
||||
hoveredItemStore.value = { node, type: 'Widget', widget }
|
||||
return showTooltip(
|
||||
widget.tooltip ?? nodeDef.input.getInput(widget.name)?.tooltip
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
clearHovered()
|
||||
hideTooltip()
|
||||
clearTimeout(idleTimeout)
|
||||
|
||||
if ((e.target as Node).nodeName !== 'CANVAS') return
|
||||
@@ -133,7 +100,7 @@ const onMouseMove = (e: MouseEvent) => {
|
||||
}
|
||||
|
||||
useEventListener(window, 'mousemove', onMouseMove)
|
||||
useEventListener(window, 'click', clearHovered)
|
||||
useEventListener(window, 'click', hideTooltip)
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -74,11 +74,7 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
|
||||
backgroundColor: litegraphColors.WIDGET_BGCOLOR
|
||||
}"
|
||||
>
|
||||
{{
|
||||
Array.isArray(nodeDef.description)
|
||||
? nodeDef.description[0]
|
||||
: nodeDef.description
|
||||
}}
|
||||
{{ nodeDef.description }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
<template>
|
||||
<div v-if="!hasAnyDoc()">Select a node to see documentation.</div>
|
||||
<div v-else-if="rawDoc" ref="docElement" v-html="rawDoc"></div>
|
||||
<div v-else ref="docElement">
|
||||
<div class="doc-node">{{ title }}</div>
|
||||
<div>{{ description }}</div>
|
||||
<div v-if="inputs.length" class="doc-section">Inputs</div>
|
||||
<div
|
||||
v-if="inputs.length"
|
||||
v-for="input in inputs"
|
||||
tabindex="-1"
|
||||
class="doc-item"
|
||||
>
|
||||
{{ input[0] }}
|
||||
<div>{{ input[1] }}</div>
|
||||
</div>
|
||||
<div v-if="outputs.length" class="doc-section">Outputs</div>
|
||||
<div
|
||||
v-if="outputs.length"
|
||||
v-for="output in outputs"
|
||||
tabindex="-1"
|
||||
class="doc-item"
|
||||
>
|
||||
{{ output[0] }}
|
||||
<div>{{ output[1] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onBeforeUnmount, isReactive } from 'vue'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useHoveredItemStore } from '@/stores/graphStore'
|
||||
const hoveredItemStore = useHoveredItemStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
const docElement = ref(null)
|
||||
|
||||
let def
|
||||
const rawDoc = ref(null)
|
||||
const description = ref(null)
|
||||
const title = ref(null)
|
||||
const inputs = ref([])
|
||||
const outputs = ref([])
|
||||
|
||||
function selectHelp(name: string, value?: string) {
|
||||
if (!docElement.value || !name) {
|
||||
return null
|
||||
}
|
||||
if (def.description[2]?.select) {
|
||||
return def.description[2].select(docElement.value, name, value)
|
||||
}
|
||||
//attempt to navigate to name in help
|
||||
function collapseUnlessMatch(items, t) {
|
||||
var match = items.querySelector('[doc_title="' + t + '"]')
|
||||
if (!match) {
|
||||
for (let i of items.children) {
|
||||
if (i.innerHTML.slice(0, t.length + 5).includes(t)) {
|
||||
match = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!match) {
|
||||
return null
|
||||
}
|
||||
//For longer documentation items with fewer collapsable elements,
|
||||
//scroll to make sure the entirety of the selected item is visible
|
||||
match.scrollIntoView({ block: 'nearest' })
|
||||
//The previous floating help implementation would try to scroll the window
|
||||
//itself if the display was partiall offscreen. As the sidebar documentation
|
||||
//does not pan with the canvas, this should no longer be needed
|
||||
//window.scrollTo(0, 0)
|
||||
for (let i of items.querySelectorAll('.doc_collapse')) {
|
||||
if (i.contains(match)) {
|
||||
setCollapse(i, false)
|
||||
} else {
|
||||
setCollapse(i, true)
|
||||
}
|
||||
}
|
||||
return match
|
||||
}
|
||||
let target = collapseUnlessMatch(docElement.value, name)
|
||||
if (target) {
|
||||
target.focus()
|
||||
if (value) {
|
||||
collapseUnlessMatch(target, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
function updateNode() {
|
||||
//Grab the topmost node.
|
||||
//current_node is topmost on screen and
|
||||
//selectedItems is unordered
|
||||
const node = app?.graph?._nodes[app?.graph?._nodes.length - 1]
|
||||
if (!node) {
|
||||
// Graph has no nodes
|
||||
return
|
||||
}
|
||||
const nodeDef = LiteGraph.getNodeType(node.type).nodeData
|
||||
if (def == nodeDef) {
|
||||
return
|
||||
}
|
||||
def = nodeDef
|
||||
title.value = def.display_name
|
||||
if (Array.isArray(def.description)) {
|
||||
rawDoc.value = def.description[1]
|
||||
outputs.value = []
|
||||
inputs.value = []
|
||||
return
|
||||
} else {
|
||||
rawDoc.value = null
|
||||
}
|
||||
description.value = def.description
|
||||
let input_temp = []
|
||||
for (let k in def?.input?.required) {
|
||||
if (def.input.required[k][1]?.tooltip) {
|
||||
input_temp.push([k, def.input.required[k][1].tooltip])
|
||||
}
|
||||
}
|
||||
for (let k in def?.optional?.required) {
|
||||
if (def.input.optional[k][1]?.tooltip) {
|
||||
input_temp.push([k, def.input.optional[k][1].tooltip])
|
||||
}
|
||||
}
|
||||
inputs.value = input_temp
|
||||
if (def.output_tooltips) {
|
||||
const outputs_temp = []
|
||||
const output_name = def.output_name || def.output
|
||||
for (let i = 0; i < def.output_tooltips.length; i++) {
|
||||
outputs_temp[i] = [output_name[i], def.output_tooltips[i]]
|
||||
}
|
||||
outputs.value = outputs_temp
|
||||
} else {
|
||||
outputs.value = []
|
||||
}
|
||||
}
|
||||
function hasAnyDoc() {
|
||||
return def?.description || inputs.value.length || outputs.value.length
|
||||
}
|
||||
watch(hoveredItemStore, (hoveredItem) => {
|
||||
if (!hoveredItem.value) {
|
||||
return
|
||||
}
|
||||
const item = hoveredItem.value
|
||||
const nodeDef = LiteGraph.getNodeType(item.node.type).nodeData
|
||||
if (nodeDef != def) {
|
||||
return
|
||||
}
|
||||
if (item.type == 'DESCRIPTION') {
|
||||
return
|
||||
} else if (item.type == 'Input') {
|
||||
selectHelp(item.inputName)
|
||||
hoveredItem.value = null
|
||||
} else if (item.type == 'Output') {
|
||||
selectHelp(nodeDef?.output?.all?.[item.outputSlot]?.name)
|
||||
hoveredItem.value = null
|
||||
} else if (item.type == 'Widget') {
|
||||
selectHelp(item.widget.name, item.widget.value)
|
||||
hoveredItem.value = null
|
||||
}
|
||||
})
|
||||
if (isReactive(canvasStore?.canvas)) {
|
||||
watch(() => canvasStore.canvas?.current_node, updateNode)
|
||||
} else {
|
||||
let interval = setInterval(updateNode, 300)
|
||||
onBeforeUnmount(() => clearInterval(this.interval))
|
||||
}
|
||||
updateNode()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.doc-node {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
.doc-section {
|
||||
background-color: var(--comfy-menu-bg);
|
||||
}
|
||||
.doc-item div {
|
||||
margin-inline-start: 1vw;
|
||||
}
|
||||
@keyframes selectAnimation {
|
||||
0% {
|
||||
background-color: #5555;
|
||||
}
|
||||
80% {
|
||||
background-color: #5555;
|
||||
}
|
||||
100% {
|
||||
background-color: #0000;
|
||||
}
|
||||
}
|
||||
.doc-item:focus {
|
||||
animation: selectAnimation 2s;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,18 @@
|
||||
<template>
|
||||
<SidebarTabTemplate :title="$t('sideToolbar.queue')">
|
||||
<template #tool-buttons>
|
||||
<Popover ref="outputFilterPopup">
|
||||
<OutputFilters />
|
||||
</Popover>
|
||||
|
||||
<Button
|
||||
icon="pi pi-filter"
|
||||
text
|
||||
severity="secondary"
|
||||
@click="outputFilterPopup.toggle($event)"
|
||||
v-tooltip="$t(`sideToolbar.queueTab.filter`)"
|
||||
:class="{ 'text-yellow-500': anyFilter }"
|
||||
/>
|
||||
<Button
|
||||
:icon="
|
||||
imageFit === 'cover'
|
||||
@@ -99,6 +111,7 @@ import Button from 'primevue/button'
|
||||
import ConfirmPopup from 'primevue/confirmpopup'
|
||||
import ContextMenu from 'primevue/contextmenu'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import Popover from 'primevue/popover'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import TaskItem from './queue/TaskItem.vue'
|
||||
import ResultGallery from './queue/ResultGallery.vue'
|
||||
@@ -111,7 +124,9 @@ import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
const IMAGE_FIT = 'Comfy.Queue.ImageFit'
|
||||
const SETTING_FIT = 'Comfy.Queue.ImageFit'
|
||||
const SETTING_FLAT = 'Comfy.Queue.ShowFlatList'
|
||||
const SETTING_FILTER = 'Comfy.Queue.Filter'
|
||||
const confirm = useConfirm()
|
||||
const toast = useToast()
|
||||
const queueStore = useQueueStore()
|
||||
@@ -120,7 +135,7 @@ const commandStore = useCommandStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
// Expanded view: show all outputs in a flat list.
|
||||
const isExpanded = ref(false)
|
||||
const isExpanded = computed<boolean>(() => settingStore.get(SETTING_FLAT))
|
||||
const visibleTasks = ref<TaskItemImpl[]>([])
|
||||
const scrollContainer = ref<HTMLElement | null>(null)
|
||||
const loadMoreTrigger = ref<HTMLElement | null>(null)
|
||||
@@ -128,10 +143,27 @@ const galleryActiveIndex = ref(-1)
|
||||
// Folder view: only show outputs from a single selected task.
|
||||
const folderTask = ref<TaskItemImpl | null>(null)
|
||||
const isInFolderView = computed(() => folderTask.value !== null)
|
||||
const imageFit = computed<string>(() => settingStore.get(IMAGE_FIT))
|
||||
const imageFit = computed<string>(() => settingStore.get(SETTING_FIT))
|
||||
const hideCached = computed<boolean>(
|
||||
() => settingStore.get(SETTING_FILTER)?.hideCached
|
||||
)
|
||||
const hideCanceled = computed<boolean>(
|
||||
() => settingStore.get(SETTING_FILTER)?.hideCanceled
|
||||
)
|
||||
const anyFilter = computed(() => hideCanceled.value || hideCached.value)
|
||||
|
||||
watch(hideCached, () => {
|
||||
updateVisibleTasks()
|
||||
})
|
||||
watch(hideCanceled, () => {
|
||||
updateVisibleTasks()
|
||||
})
|
||||
|
||||
const outputFilterPopup = ref(null)
|
||||
|
||||
const ITEMS_PER_PAGE = 8
|
||||
const SCROLL_THRESHOLD = 100 // pixels from bottom to trigger load
|
||||
const MAX_LOAD_ITERATIONS = 10
|
||||
|
||||
const allTasks = computed(() =>
|
||||
isInFolderView.value
|
||||
@@ -149,21 +181,48 @@ const allGalleryItems = computed(() =>
|
||||
})
|
||||
)
|
||||
|
||||
const loadMoreItems = () => {
|
||||
const filterTasks = (tasks: TaskItemImpl[]) =>
|
||||
tasks.filter((t) => {
|
||||
if (
|
||||
hideCanceled.value &&
|
||||
t.status?.messages?.at(-1)?.[0] === 'execution_interrupted'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
hideCached.value &&
|
||||
t.flatOutputs?.length &&
|
||||
t.flatOutputs.every((o) => o.cached)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
const loadMoreItems = (iteration: number) => {
|
||||
const currentLength = visibleTasks.value.length
|
||||
const newTasks = allTasks.value.slice(
|
||||
const newTasks = filterTasks(allTasks.value).slice(
|
||||
currentLength,
|
||||
currentLength + ITEMS_PER_PAGE
|
||||
)
|
||||
visibleTasks.value.push(...newTasks)
|
||||
// If we've added some items, check if we need to add more
|
||||
// Prevent loading everything at once in case of render update issues
|
||||
if (newTasks.length && iteration < MAX_LOAD_ITERATIONS) {
|
||||
nextTick(() => {
|
||||
checkAndLoadMore(iteration + 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const checkAndLoadMore = () => {
|
||||
const checkAndLoadMore = (iteration: number) => {
|
||||
if (!scrollContainer.value) return
|
||||
|
||||
const { scrollHeight, scrollTop, clientHeight } = scrollContainer.value
|
||||
if (scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD) {
|
||||
loadMoreItems()
|
||||
loadMoreItems(iteration)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +230,7 @@ useInfiniteScroll(
|
||||
scrollContainer,
|
||||
() => {
|
||||
if (visibleTasks.value.length < allTasks.value.length) {
|
||||
loadMoreItems()
|
||||
loadMoreItems(0)
|
||||
}
|
||||
},
|
||||
{ distance: SCROLL_THRESHOLD }
|
||||
@@ -181,16 +240,20 @@ useInfiniteScroll(
|
||||
// This is necessary as the sidebar tab can change size when user drags the splitter.
|
||||
useResizeObserver(scrollContainer, () => {
|
||||
nextTick(() => {
|
||||
checkAndLoadMore()
|
||||
checkAndLoadMore(0)
|
||||
})
|
||||
})
|
||||
|
||||
const updateVisibleTasks = () => {
|
||||
visibleTasks.value = allTasks.value.slice(0, ITEMS_PER_PAGE)
|
||||
visibleTasks.value = filterTasks(allTasks.value).slice(0, ITEMS_PER_PAGE)
|
||||
|
||||
nextTick(() => {
|
||||
checkAndLoadMore(0)
|
||||
})
|
||||
}
|
||||
|
||||
const toggleExpanded = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
settingStore.set(SETTING_FLAT, !isExpanded.value)
|
||||
updateVisibleTasks()
|
||||
}
|
||||
|
||||
@@ -291,7 +354,10 @@ const exitFolderView = () => {
|
||||
}
|
||||
|
||||
const toggleImageFit = () => {
|
||||
settingStore.set(IMAGE_FIT, imageFit.value === 'cover' ? 'contain' : 'cover')
|
||||
settingStore.set(
|
||||
SETTING_FIT,
|
||||
imageFit.value === 'cover' ? 'contain' : 'cover'
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -95,18 +95,11 @@
|
||||
renderTreeNode(workflowsTree, WorkflowTreeType.Browse).children
|
||||
"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
v-if="workflowStore.persistedWorkflows.length > 0"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
<WorkflowTreeLeaf :node="node" />
|
||||
</template>
|
||||
</TreeExplorer>
|
||||
<NoResultsPlaceholder
|
||||
v-else
|
||||
icon="pi pi-folder"
|
||||
:title="$t('empty')"
|
||||
:message="$t('noWorkflowsFound')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comfyui-workflows-search-panel" v-else>
|
||||
@@ -127,7 +120,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import WorkflowTreeLeaf from '@/components/sidebar/tabs/workflows/WorkflowTreeLeaf.vue'
|
||||
import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue'
|
||||
import TreeExplorer from '@/components/common/TreeExplorer.vue'
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="mx-6 mb-4" v-if="inProgressDownloads.length > 0">
|
||||
<div class="mx-6 mb-4" v-if="downloads.length > 0">
|
||||
<div class="text-lg my-4">
|
||||
{{ $t('electronFileDownload.inProgress') }}
|
||||
</div>
|
||||
|
||||
<template v-for="download in inProgressDownloads" :key="download.url">
|
||||
<template v-for="download in downloads" :key="download.url">
|
||||
<DownloadItem :download="download" />
|
||||
</template>
|
||||
</div>
|
||||
@@ -16,5 +16,5 @@ import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const electronDownloadStore = useElectronDownloadStore()
|
||||
const { inProgressDownloads } = storeToRefs(electronDownloadStore)
|
||||
const { downloads } = storeToRefs(electronDownloadStore)
|
||||
</script>
|
||||
|
||||
38
src/components/sidebar/tabs/queue/OutputFilters.vue
Normal file
38
src/components/sidebar/tabs/queue/OutputFilters.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="flex items-center gap-2">
|
||||
{{ $t('sideToolbar.queueTab.filters.hideCached') }}
|
||||
<ToggleSwitch v-model="hideCached" />
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
{{ $t('sideToolbar.queueTab.filters.hideCanceled') }}
|
||||
<ToggleSwitch v-model="hideCanceled" />
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
const SETTING_FILTER = 'Comfy.Queue.Filter'
|
||||
|
||||
const { t } = useI18n()
|
||||
const settingStore = useSettingStore()
|
||||
const filter = settingStore.get(SETTING_FILTER) ?? {}
|
||||
|
||||
const createCompute = (k: string) =>
|
||||
computed({
|
||||
get() {
|
||||
return filter[k]
|
||||
},
|
||||
set(value) {
|
||||
filter[k] = value
|
||||
settingStore.set(SETTING_FILTER, filter)
|
||||
}
|
||||
})
|
||||
|
||||
const hideCached = createCompute('hideCached')
|
||||
const hideCanceled = createCompute('hideCanceled')
|
||||
</script>
|
||||
@@ -31,16 +31,20 @@
|
||||
|
||||
<div class="task-item-details">
|
||||
<div class="tag-wrapper status-tag-group">
|
||||
<Tag v-if="isFlatTask && task.isHistory" class="node-name-tag">
|
||||
<Tag v-if="isFlatTask && task.isHistory && node" class="node-name-tag">
|
||||
<Button
|
||||
class="task-node-link"
|
||||
:label="`${node?.type} (#${node?.id})`"
|
||||
:label="`${node.type} (#${node.id})`"
|
||||
link
|
||||
size="small"
|
||||
@click="app.goToNode(node?.id)"
|
||||
/>
|
||||
</Tag>
|
||||
<Tag :severity="taskTagSeverity(task.displayStatus)">
|
||||
<Tag
|
||||
:severity="taskTagSeverity(task.displayStatus)"
|
||||
class="task-duration relative"
|
||||
>
|
||||
<i v-if="isCachedResult" class="pi pi-server task-cached-icon"></i>
|
||||
<span v-html="taskStatusText(task.displayStatus)"></span>
|
||||
<span v-if="task.isHistory" class="task-time">
|
||||
{{ formatTime(task.executionTimeInSeconds) }}
|
||||
@@ -90,6 +94,7 @@ const node: ComfyNode | null =
|
||||
) ?? null
|
||||
: null
|
||||
const progressPreviewBlobUrl = ref('')
|
||||
const isCachedResult = props.isFlatTask && coverResult?.cached
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
@@ -142,7 +147,7 @@ const taskStatusText = (status: TaskItemDisplayStatus) => {
|
||||
case TaskItemDisplayStatus.Running:
|
||||
return '<i class="pi pi-spin pi-spinner" style="font-weight: bold"></i> Running'
|
||||
case TaskItemDisplayStatus.Completed:
|
||||
return '<i class="pi pi-check" style="font-weight: bold"></i>'
|
||||
return `<i class="pi pi-check${isCachedResult ? ' cached' : ''}" style="font-weight: bold"></i>`
|
||||
case TaskItemDisplayStatus.Failed:
|
||||
return 'Failed'
|
||||
case TaskItemDisplayStatus.Cancelled:
|
||||
@@ -226,4 +231,15 @@ are floating on top of images. */
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.task-cached-icon {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
:deep(.pi-check.cached) {
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -198,6 +198,25 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
type: 'hidden',
|
||||
defaultValue: 'cover'
|
||||
},
|
||||
// Hidden setting used by the queue for if the results should be grouped by prompt
|
||||
{
|
||||
id: 'Comfy.Queue.ShowFlatList',
|
||||
name: 'Queue show flat list',
|
||||
type: 'hidden',
|
||||
defaultValue: false
|
||||
},
|
||||
|
||||
// Hidden setting used by the queue to filter certain results
|
||||
{
|
||||
id: 'Comfy.Queue.Filter',
|
||||
name: 'Queue output filters',
|
||||
type: 'hidden',
|
||||
defaultValue: {
|
||||
hideCanceled: false,
|
||||
hideCached: false
|
||||
},
|
||||
versionAdded: '1.4.3'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.GroupSelectedNodes.Padding',
|
||||
category: ['LiteGraph', 'Group', 'Padding'],
|
||||
@@ -623,18 +642,5 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
type: 'hidden',
|
||||
defaultValue: {} as Record<string, string>,
|
||||
versionAdded: '1.4.8'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Queue.MaxHistoryItems',
|
||||
name: 'Queue history size',
|
||||
tooltip: 'The maximum number of tasks that show in the queue history.',
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 16,
|
||||
max: 256,
|
||||
step: 16
|
||||
},
|
||||
defaultValue: 64,
|
||||
versionAdded: '1.4.12'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -10,17 +10,15 @@ import {
|
||||
VramManagement
|
||||
} from '@/types/serverArgs'
|
||||
|
||||
export type ServerConfigValue = string | number | true | null | undefined
|
||||
|
||||
export interface ServerConfig<T> extends FormItem {
|
||||
id: string
|
||||
defaultValue: T
|
||||
category?: string[]
|
||||
// Override the default value getter with a custom function.
|
||||
getValue?: (value: T) => Record<string, ServerConfigValue>
|
||||
getValue?: (value: T) => Record<string, any>
|
||||
}
|
||||
|
||||
export const WEB_ONLY_CONFIG_ITEMS: ServerConfig<ServerConfigValue>[] = [
|
||||
export const WEB_ONLY_CONFIG_ITEMS: ServerConfig<any>[] = [
|
||||
// We only need these settings in the web version. Desktop app manages them already.
|
||||
{
|
||||
id: 'listen',
|
||||
@@ -45,21 +43,21 @@ export const SERVER_CONFIG_ITEMS: ServerConfig<any>[] = [
|
||||
name: 'TLS Key File: Path to TLS key file for HTTPS',
|
||||
category: ['Network'],
|
||||
type: 'text',
|
||||
defaultValue: ''
|
||||
defaultValue: undefined
|
||||
},
|
||||
{
|
||||
id: 'tls-certfile',
|
||||
name: 'TLS Certificate File: Path to TLS certificate file for HTTPS',
|
||||
category: ['Network'],
|
||||
type: 'text',
|
||||
defaultValue: ''
|
||||
defaultValue: undefined
|
||||
},
|
||||
{
|
||||
id: 'enable-cors-header',
|
||||
name: 'Enable CORS header: Use "*" for all origins or specify domain',
|
||||
category: ['Network'],
|
||||
type: 'text',
|
||||
defaultValue: ''
|
||||
defaultValue: undefined
|
||||
},
|
||||
{
|
||||
id: 'max-upload-size',
|
||||
@@ -99,7 +97,7 @@ export const SERVER_CONFIG_ITEMS: ServerConfig<any>[] = [
|
||||
name: 'CUDA device index to use',
|
||||
category: ['CUDA'],
|
||||
type: 'number',
|
||||
defaultValue: null
|
||||
defaultValue: undefined
|
||||
},
|
||||
{
|
||||
id: 'cuda-malloc',
|
||||
@@ -163,8 +161,6 @@ export const SERVER_CONFIG_ITEMS: ServerConfig<any>[] = [
|
||||
type: 'combo',
|
||||
options: [
|
||||
FloatingPointPrecision.AUTO,
|
||||
FloatingPointPrecision.FP64,
|
||||
FloatingPointPrecision.FP32,
|
||||
FloatingPointPrecision.FP16,
|
||||
FloatingPointPrecision.BF16,
|
||||
FloatingPointPrecision.FP8E4M3FN,
|
||||
@@ -257,7 +253,7 @@ export const SERVER_CONFIG_ITEMS: ServerConfig<any>[] = [
|
||||
name: 'DirectML device index',
|
||||
category: ['Memory'],
|
||||
type: 'number',
|
||||
defaultValue: null
|
||||
defaultValue: undefined
|
||||
},
|
||||
{
|
||||
id: 'disable-ipex-optimize',
|
||||
@@ -299,10 +295,10 @@ export const SERVER_CONFIG_ITEMS: ServerConfig<any>[] = [
|
||||
},
|
||||
{
|
||||
id: 'cache-lru',
|
||||
name: 'Use LRU caching with a maximum of N node results cached.',
|
||||
name: 'Use LRU caching with a maximum of N node results cached. (0 to disable).',
|
||||
category: ['Cache'],
|
||||
type: 'number',
|
||||
defaultValue: null,
|
||||
defaultValue: 0,
|
||||
tooltip: 'May use more RAM/VRAM.'
|
||||
},
|
||||
|
||||
@@ -370,7 +366,7 @@ export const SERVER_CONFIG_ITEMS: ServerConfig<any>[] = [
|
||||
name: 'Reserved VRAM (GB)',
|
||||
category: ['Memory'],
|
||||
type: 'number',
|
||||
defaultValue: null,
|
||||
defaultValue: undefined,
|
||||
tooltip:
|
||||
'Set the amount of vram in GB you want to reserve for use by your OS/other software. By default some amount is reverved depending on your OS.'
|
||||
},
|
||||
|
||||
@@ -17,11 +17,9 @@ app.registerExtension({
|
||||
if (node.widgets) {
|
||||
// Locate dynamic prompt text widgets
|
||||
// Include any widgets with dynamicPrompts set to true, and customtext
|
||||
// @ts-expect-error dynamicPrompts is not typed
|
||||
const widgets = node.widgets.filter((n) => n.dynamicPrompts)
|
||||
for (const widget of widgets) {
|
||||
// Override the serialization of the value to resolve dynamic prompts for all widgets supporting it in this node
|
||||
// @ts-expect-error hacky override
|
||||
widget.serializeValue = (workflowNode, widgetIndex) => {
|
||||
let prompt = stripComments(widget.value)
|
||||
while (
|
||||
|
||||
@@ -107,14 +107,6 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
|
||||
// TODO(huchenlei): Add a confirmation dialog.
|
||||
electronAPI.reinstall()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy-Desktop.Restart',
|
||||
label: 'Restart',
|
||||
icon: 'pi pi-refresh',
|
||||
function() {
|
||||
electronAPI.restartApp()
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// @ts-strict-ignore
|
||||
import { app } from '@/scripts/app'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
|
||||
import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
|
||||
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
|
||||
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
|
||||
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
|
||||
@@ -185,49 +185,6 @@ class Load3d {
|
||||
this.startAnimation()
|
||||
}
|
||||
|
||||
getCameraState() {
|
||||
const currentType = this.getCurrentCameraType()
|
||||
return {
|
||||
position: this.activeCamera.position.clone(),
|
||||
target: this.controls.target.clone(),
|
||||
zoom:
|
||||
this.activeCamera instanceof THREE.OrthographicCamera
|
||||
? this.activeCamera.zoom
|
||||
: (this.activeCamera as THREE.PerspectiveCamera).zoom,
|
||||
cameraType: currentType
|
||||
}
|
||||
}
|
||||
|
||||
setCameraState(state: {
|
||||
position: THREE.Vector3
|
||||
target: THREE.Vector3
|
||||
zoom: number
|
||||
cameraType: 'perspective' | 'orthographic'
|
||||
}) {
|
||||
if (
|
||||
this.activeCamera !==
|
||||
(state.cameraType === 'perspective'
|
||||
? this.perspectiveCamera
|
||||
: this.orthographicCamera)
|
||||
) {
|
||||
this.toggleCamera(state.cameraType)
|
||||
}
|
||||
|
||||
this.activeCamera.position.copy(state.position)
|
||||
|
||||
this.controls.target.copy(state.target)
|
||||
|
||||
if (this.activeCamera instanceof THREE.OrthographicCamera) {
|
||||
this.activeCamera.zoom = state.zoom
|
||||
this.activeCamera.updateProjectionMatrix()
|
||||
} else if (this.activeCamera instanceof THREE.PerspectiveCamera) {
|
||||
this.activeCamera.zoom = state.zoom
|
||||
this.activeCamera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
this.controls.update()
|
||||
}
|
||||
|
||||
setUpDirection(
|
||||
direction: 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z'
|
||||
) {
|
||||
@@ -401,24 +358,26 @@ class Load3d {
|
||||
toggleCamera(cameraType?: 'perspective' | 'orthographic') {
|
||||
const oldCamera = this.activeCamera
|
||||
|
||||
const position = oldCamera.position.clone()
|
||||
const rotation = oldCamera.rotation.clone()
|
||||
const position = this.activeCamera.position.clone()
|
||||
const rotation = this.activeCamera.rotation.clone()
|
||||
const target = this.controls.target.clone()
|
||||
|
||||
if (!cameraType) {
|
||||
this.activeCamera =
|
||||
oldCamera === this.perspectiveCamera
|
||||
this.activeCamera === this.perspectiveCamera
|
||||
? this.orthographicCamera
|
||||
: this.perspectiveCamera
|
||||
} else {
|
||||
this.activeCamera =
|
||||
const requestedCamera =
|
||||
cameraType === 'perspective'
|
||||
? this.perspectiveCamera
|
||||
: this.orthographicCamera
|
||||
|
||||
if (oldCamera === this.activeCamera) {
|
||||
if (this.activeCamera === requestedCamera) {
|
||||
return
|
||||
}
|
||||
|
||||
this.activeCamera = requestedCamera
|
||||
}
|
||||
|
||||
this.activeCamera.position.copy(position)
|
||||
@@ -435,12 +394,6 @@ class Load3d {
|
||||
this.handleResize()
|
||||
}
|
||||
|
||||
getCurrentCameraType(): 'perspective' | 'orthographic' {
|
||||
return this.activeCamera === this.perspectiveCamera
|
||||
? 'perspective'
|
||||
: 'orthographic'
|
||||
}
|
||||
|
||||
toggleGrid(showGrid: boolean) {
|
||||
if (this.gridHelper) {
|
||||
this.gridHelper.visible = showGrid
|
||||
@@ -1035,16 +988,11 @@ function configureLoad3D(
|
||||
bgColor: IWidget,
|
||||
lightIntensity: IWidget,
|
||||
upDirection: IWidget,
|
||||
cameraState?: any,
|
||||
postModelUpdateFunc?: (load3d: Load3d) => void
|
||||
) {
|
||||
const createModelUpdateHandler = () => {
|
||||
let isFirstLoad = true
|
||||
|
||||
return async (value: string | number | boolean | object) => {
|
||||
if (!value) return
|
||||
|
||||
const filename = value as string
|
||||
const onModelWidgetUpdate = async () => {
|
||||
if (modelWidget.value) {
|
||||
const filename = modelWidget.value as string
|
||||
const modelUrl = api.apiURL(
|
||||
getResourceURL(...splitFilePath(filename), loadFolder)
|
||||
)
|
||||
@@ -1069,22 +1017,11 @@ function configureLoad3D(
|
||||
if (postModelUpdateFunc) {
|
||||
postModelUpdateFunc(load3d)
|
||||
}
|
||||
|
||||
if (isFirstLoad && cameraState && typeof cameraState === 'object') {
|
||||
try {
|
||||
load3d.setCameraState(cameraState)
|
||||
} catch (error) {
|
||||
console.warn('Failed to restore camera state:', error)
|
||||
}
|
||||
isFirstLoad = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onModelWidgetUpdate = createModelUpdateHandler()
|
||||
|
||||
if (modelWidget.value) {
|
||||
onModelWidgetUpdate(modelWidget.value)
|
||||
onModelWidgetUpdate()
|
||||
}
|
||||
|
||||
modelWidget.callback = onModelWidgetUpdate
|
||||
@@ -1142,8 +1079,6 @@ app.registerExtension({
|
||||
LOAD_3D(node, inputName) {
|
||||
let load3dNode = app.graph._nodes.filter((wi) => wi.type == 'Load3D')
|
||||
|
||||
node.addProperty('Camera Info', '')
|
||||
|
||||
const container = document.createElement('div')
|
||||
container.id = `comfy-load-3d-${load3dNode.length}`
|
||||
container.classList.add('comfy-load-3d')
|
||||
@@ -1277,21 +1212,6 @@ app.registerExtension({
|
||||
(w: IWidget) => w.name === 'up_direction'
|
||||
)
|
||||
|
||||
let cameraState
|
||||
try {
|
||||
const cameraInfo = node.properties['Camera Info']
|
||||
if (
|
||||
cameraInfo &&
|
||||
typeof cameraInfo === 'string' &&
|
||||
cameraInfo.trim() !== ''
|
||||
) {
|
||||
cameraState = JSON.parse(cameraInfo)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse camera state:', error)
|
||||
cameraState = undefined
|
||||
}
|
||||
|
||||
configureLoad3D(
|
||||
load3d,
|
||||
'input',
|
||||
@@ -1302,17 +1222,13 @@ app.registerExtension({
|
||||
material,
|
||||
bgColor,
|
||||
lightIntensity,
|
||||
upDirection,
|
||||
cameraState
|
||||
upDirection
|
||||
)
|
||||
|
||||
const w = node.widgets.find((w: IWidget) => w.name === 'width')
|
||||
const h = node.widgets.find((w: IWidget) => w.name === 'height')
|
||||
|
||||
// @ts-expect-error hacky override
|
||||
sceneWidget.serializeValue = async () => {
|
||||
node.properties['Camera Info'] = JSON.stringify(load3d.getCameraState())
|
||||
|
||||
const imageData = await load3d.captureScene(w.value, h.value)
|
||||
|
||||
const blob = await fetch(imageData).then((r) => r.blob())
|
||||
@@ -1351,8 +1267,6 @@ app.registerExtension({
|
||||
(wi) => wi.type == 'Load3DAnimation'
|
||||
)
|
||||
|
||||
node.addProperty('Camera Info', '')
|
||||
|
||||
const container = document.createElement('div')
|
||||
container.id = `comfy-load-3d-animation-${load3dNode.length}`
|
||||
container.classList.add('comfy-load-3d-animation')
|
||||
@@ -1552,21 +1466,6 @@ app.registerExtension({
|
||||
}
|
||||
}
|
||||
|
||||
let cameraState
|
||||
try {
|
||||
const cameraInfo = node.properties['Camera Info']
|
||||
if (
|
||||
cameraInfo &&
|
||||
typeof cameraInfo === 'string' &&
|
||||
cameraInfo.trim() !== ''
|
||||
) {
|
||||
cameraState = JSON.parse(cameraInfo)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse camera state:', error)
|
||||
cameraState = undefined
|
||||
}
|
||||
|
||||
configureLoad3D(
|
||||
load3d,
|
||||
'input',
|
||||
@@ -1578,7 +1477,6 @@ app.registerExtension({
|
||||
bgColor,
|
||||
lightIntensity,
|
||||
upDirection,
|
||||
cameraState,
|
||||
(load3d: Load3d) => {
|
||||
const animationLoad3d = load3d as Load3dAnimation
|
||||
const names = animationLoad3d.getAnimationNames()
|
||||
@@ -1597,10 +1495,7 @@ app.registerExtension({
|
||||
const w = node.widgets.find((w: IWidget) => w.name === 'width')
|
||||
const h = node.widgets.find((w: IWidget) => w.name === 'height')
|
||||
|
||||
// @ts-expect-error hacky override
|
||||
sceneWidget.serializeValue = async () => {
|
||||
node.properties['Camera Info'] = JSON.stringify(load3d.getCameraState())
|
||||
|
||||
load3d.toggleAnimation(false)
|
||||
|
||||
const imageData = await load3d.captureScene(w.value, h.value)
|
||||
|
||||
@@ -275,7 +275,11 @@ var styles = `
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
transition: background-color 0.2s;
|
||||
transition: background-color border 0.2s;
|
||||
}
|
||||
.maskEditor_toolPanelContainer:hover {
|
||||
background-color: var(--p-overlaybadge-outline-color);
|
||||
border: none;
|
||||
}
|
||||
.maskEditor_toolPanelContainerSelected svg {
|
||||
fill: var(--p-button-text-primary-color) !important;
|
||||
@@ -288,15 +292,6 @@ var styles = `
|
||||
aspect-ratio: 1/1;
|
||||
fill: var(--p-button-text-secondary-color);
|
||||
}
|
||||
|
||||
.maskEditor_toolPanelContainerDark:hover {
|
||||
background-color: var(--p-surface-800);
|
||||
}
|
||||
|
||||
.maskEditor_toolPanelContainerLight:hover {
|
||||
background-color: var(--p-surface-300);
|
||||
}
|
||||
|
||||
.maskEditor_toolPanelIndicator {
|
||||
display: none;
|
||||
height: 100%;
|
||||
@@ -384,56 +379,16 @@ var styles = `
|
||||
}
|
||||
#maskEditor_topBarShortcutsContainer {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.maskEditor_topPanelIconButton_dark {
|
||||
width: 50px;
|
||||
.maskEditor_topPanelIconButton {
|
||||
width: 53.3px;
|
||||
height: 30px;
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: background-color 0.1s;
|
||||
background: var(--p-surface-800);
|
||||
border: 1px solid var(--p-form-field-border-color);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.maskEditor_topPanelIconButton_dark:hover {
|
||||
background-color: var(--p-surface-900);
|
||||
}
|
||||
|
||||
.maskEditor_topPanelIconButton_dark svg {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
pointer-events: none;
|
||||
fill: var(--input-text);
|
||||
}
|
||||
|
||||
.maskEditor_topPanelIconButton_light {
|
||||
width: 50px;
|
||||
height: 30px;
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: background-color 0.1s;
|
||||
background: var(--comfy-menu-bg);
|
||||
border: 1px solid var(--p-form-field-border-color);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.maskEditor_topPanelIconButton_light:hover {
|
||||
background-color: var(--p-surface-300);
|
||||
}
|
||||
|
||||
.maskEditor_topPanelIconButton_light svg {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
pointer-events: none;
|
||||
fill: var(--input-text);
|
||||
}
|
||||
|
||||
.maskEditor_topPanelButton_dark {
|
||||
@@ -702,24 +657,6 @@ var styles = `
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.maskEditor_toolPanelZoomIndicator {
|
||||
width: var(--sidebar-width);
|
||||
height: var(--sidebar-width);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: var(--p-button-text-secondary-color);
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
#maskEditor_toolPanelDimensionsText {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
`
|
||||
|
||||
var styleSheet = document.createElement('style')
|
||||
@@ -1292,8 +1229,6 @@ class PaintBucketTool {
|
||||
this.messageBroker.subscribe('paintBucketFill', (point: Point) =>
|
||||
this.floodFill(point)
|
||||
)
|
||||
|
||||
this.messageBroker.subscribe('invert', () => this.invertMask())
|
||||
}
|
||||
|
||||
private addPullTopics() {
|
||||
@@ -1439,48 +1374,6 @@ class PaintBucketTool {
|
||||
getTolerance(): number {
|
||||
return this.tolerance
|
||||
}
|
||||
|
||||
//invert mask
|
||||
|
||||
private invertMask() {
|
||||
const imageData = this.ctx.getImageData(
|
||||
0,
|
||||
0,
|
||||
this.canvas.width,
|
||||
this.canvas.height
|
||||
)
|
||||
const data = imageData.data
|
||||
|
||||
// Find first non-transparent pixel to get mask color
|
||||
let maskR = 0,
|
||||
maskG = 0,
|
||||
maskB = 0
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
if (data[i + 3] > 0) {
|
||||
maskR = data[i]
|
||||
maskG = data[i + 1]
|
||||
maskB = data[i + 2]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Process each pixel
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const alpha = data[i + 3]
|
||||
// Invert alpha channel (0 becomes 255, 255 becomes 0)
|
||||
data[i + 3] = 255 - alpha
|
||||
|
||||
// If this was originally transparent (now opaque), fill with mask color
|
||||
if (alpha === 0) {
|
||||
data[i] = maskR
|
||||
data[i + 1] = maskG
|
||||
data[i + 2] = maskB
|
||||
}
|
||||
}
|
||||
|
||||
this.ctx.putImageData(imageData, 0, 0)
|
||||
this.messageBroker.publish('saveState')
|
||||
}
|
||||
}
|
||||
|
||||
class ColorSelectTool {
|
||||
@@ -1916,15 +1809,10 @@ class BrushTool {
|
||||
smoothingLastDrawTime!: Date
|
||||
maskCtx: CanvasRenderingContext2D | null = null
|
||||
|
||||
brushStrokeCanvas: HTMLCanvasElement | null = null
|
||||
brushStrokeCtx: CanvasRenderingContext2D | null = null
|
||||
|
||||
//brush adjustment
|
||||
isBrushAdjusting: boolean = false
|
||||
brushPreviewGradient: HTMLElement | null = null
|
||||
initialPoint: Point | null = null
|
||||
useDominantAxis: boolean = false
|
||||
brushAdjustmentSpeed: number = 1.0
|
||||
|
||||
maskEditor: MaskEditorDialog
|
||||
messageBroker: MessageBroker
|
||||
@@ -1935,13 +1823,6 @@ class BrushTool {
|
||||
this.createListeners()
|
||||
this.addPullTopics()
|
||||
|
||||
this.useDominantAxis = app.extensionManager.setting.get(
|
||||
'Comfy.MaskEditor.UseDominantAxis'
|
||||
)
|
||||
this.brushAdjustmentSpeed = app.extensionManager.setting.get(
|
||||
'Comfy.MaskEditor.BrushAdjustmentSpeed'
|
||||
)
|
||||
|
||||
this.brushSettings = {
|
||||
size: 10,
|
||||
opacity: 100,
|
||||
@@ -1979,7 +1860,7 @@ class BrushTool {
|
||||
)
|
||||
//drawing
|
||||
this.messageBroker.subscribe('drawStart', (event: PointerEvent) =>
|
||||
this.startDrawing(event)
|
||||
this.start_drawing(event)
|
||||
)
|
||||
this.messageBroker.subscribe('draw', (event: PointerEvent) =>
|
||||
this.handleDrawing(event)
|
||||
@@ -2016,27 +1897,12 @@ class BrushTool {
|
||||
)
|
||||
}
|
||||
|
||||
private async createBrushStrokeCanvas() {
|
||||
if (this.brushStrokeCanvas !== null) {
|
||||
return
|
||||
}
|
||||
|
||||
const maskCanvas = await this.messageBroker.pull('maskCanvas')
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = maskCanvas.width
|
||||
canvas.height = maskCanvas.height
|
||||
|
||||
this.brushStrokeCanvas = canvas
|
||||
this.brushStrokeCtx = canvas.getContext('2d')!
|
||||
}
|
||||
|
||||
private async startDrawing(event: PointerEvent) {
|
||||
private async start_drawing(event: PointerEvent) {
|
||||
this.isDrawing = true
|
||||
let compositionOp: CompositionOperation
|
||||
let currentTool = await this.messageBroker.pull('currentTool')
|
||||
let coords = { x: event.offsetX, y: event.offsetY }
|
||||
let coords_canvas = await this.messageBroker.pull('screenToCanvas', coords)
|
||||
await this.createBrushStrokeCanvas()
|
||||
|
||||
//set drawing mode
|
||||
if (currentTool === Tools.Eraser || event.buttons == 2) {
|
||||
@@ -2065,6 +1931,15 @@ class BrushTool {
|
||||
let coords_canvas = await this.messageBroker.pull('screenToCanvas', coords)
|
||||
let currentTool = await this.messageBroker.pull('currentTool')
|
||||
|
||||
/* move to draw
|
||||
if (event instanceof PointerEvent && event.pointerType == 'pen') {
|
||||
brush_size *= event.pressure
|
||||
this.last_pressure = event.pressure
|
||||
} else {
|
||||
brush_size = this.brush_size //this is the problem with pen pressure
|
||||
}
|
||||
*/
|
||||
|
||||
if (diff > 20 && !this.isDrawing)
|
||||
requestAnimationFrame(() => {
|
||||
this.init_shape(CompositionOperation.SourceOver)
|
||||
@@ -2176,62 +2051,29 @@ class BrushTool {
|
||||
|
||||
private async handleBrushAdjustment(event: PointerEvent) {
|
||||
const coords = { x: event.offsetX, y: event.offsetY }
|
||||
const brushDeadZone = 5
|
||||
let coords_canvas = await this.messageBroker.pull('screenToCanvas', coords)
|
||||
|
||||
const delta_x = coords_canvas.x - this.initialPoint!.x
|
||||
const delta_y = coords_canvas.y - this.initialPoint!.y
|
||||
|
||||
const effectiveDeltaX = Math.abs(delta_x) < brushDeadZone ? 0 : delta_x
|
||||
const effectiveDeltaY = Math.abs(delta_y) < brushDeadZone ? 0 : delta_y
|
||||
|
||||
// New dominant axis logic
|
||||
let finalDeltaX = effectiveDeltaX
|
||||
let finalDeltaY = effectiveDeltaY
|
||||
|
||||
console.log(this.useDominantAxis)
|
||||
|
||||
if (this.useDominantAxis) {
|
||||
// New setting flag
|
||||
const ratio = Math.abs(effectiveDeltaX) / Math.abs(effectiveDeltaY)
|
||||
const threshold = 2.0 // Configurable threshold
|
||||
|
||||
if (ratio > threshold) {
|
||||
finalDeltaY = 0 // X is dominant
|
||||
} else if (ratio < 1 / threshold) {
|
||||
finalDeltaX = 0 // Y is dominant
|
||||
}
|
||||
}
|
||||
|
||||
const cappedDeltaX = Math.max(-100, Math.min(100, finalDeltaX))
|
||||
const cappedDeltaY = Math.max(-100, Math.min(100, finalDeltaY))
|
||||
|
||||
// Rest of the function remains the same
|
||||
const sizeDelta = cappedDeltaX / 40
|
||||
const hardnessDelta = cappedDeltaY / 800
|
||||
|
||||
// Adjust brush size (horizontal movement)
|
||||
const newSize = Math.max(
|
||||
1,
|
||||
Math.min(
|
||||
100,
|
||||
this.brushSettings.size! +
|
||||
(cappedDeltaX / 35) * this.brushAdjustmentSpeed
|
||||
)
|
||||
Math.min(100, this.brushSettings.size! + delta_x / 10)
|
||||
)
|
||||
|
||||
// Adjust brush hardness (vertical movement)
|
||||
const newHardness = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
1,
|
||||
this.brushSettings!.hardness -
|
||||
(cappedDeltaY / 4000) * this.brushAdjustmentSpeed
|
||||
)
|
||||
Math.min(1, this.brushSettings!.hardness - delta_y / 200)
|
||||
)
|
||||
|
||||
this.brushSettings.size = newSize
|
||||
this.brushSettings.hardness = newHardness
|
||||
|
||||
this.messageBroker.publish('updateBrushPreview')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//helper functions
|
||||
@@ -2421,8 +2263,8 @@ class BrushTool {
|
||||
}
|
||||
|
||||
private setBrushSmoothingPrecision(precision: number) {
|
||||
//console.log('precision', precision)
|
||||
this.smoothingPrecision = precision
|
||||
console.log('precision', precision)
|
||||
// this.brushSettings.smoothingPrecision = precision
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2458,9 +2300,6 @@ class UIManager {
|
||||
private mask_opacity: number = 0.7
|
||||
private maskBlendMode: MaskBlendMode = MaskBlendMode.Black
|
||||
|
||||
private zoomTextHTML!: HTMLSpanElement
|
||||
private dimensionsTextHTML!: HTMLSpanElement
|
||||
|
||||
constructor(rootElement: HTMLElement, maskEditor: MaskEditorDialog) {
|
||||
this.rootElement = rootElement
|
||||
this.maskEditor = maskEditor
|
||||
@@ -2493,10 +2332,6 @@ class UIManager {
|
||||
)
|
||||
|
||||
this.messageBroker.subscribe('updateCursor', () => this.updateCursor())
|
||||
|
||||
this.messageBroker.subscribe('setZoomText', (text: string) =>
|
||||
this.setZoomText(text)
|
||||
)
|
||||
}
|
||||
|
||||
addPullTopics() {
|
||||
@@ -3140,17 +2975,11 @@ class UIManager {
|
||||
return separator
|
||||
}
|
||||
|
||||
//----------------
|
||||
|
||||
private async createTopBar() {
|
||||
const buttonAccentColor = this.darkMode
|
||||
? 'maskEditor_topPanelButton_dark'
|
||||
: 'maskEditor_topPanelButton_light'
|
||||
|
||||
const iconButtonAccentColor = this.darkMode
|
||||
? 'maskEditor_topPanelIconButton_dark'
|
||||
: 'maskEditor_topPanelIconButton_light'
|
||||
|
||||
var top_bar = document.createElement('div')
|
||||
top_bar.id = 'maskEditor_topBar'
|
||||
|
||||
@@ -3168,9 +2997,9 @@ class UIManager {
|
||||
|
||||
var top_bar_undo_button = document.createElement('div')
|
||||
top_bar_undo_button.id = 'maskEditor_topBarUndoButton'
|
||||
top_bar_undo_button.classList.add(iconButtonAccentColor)
|
||||
top_bar_undo_button.classList.add('maskEditor_topPanelIconButton')
|
||||
top_bar_undo_button.innerHTML =
|
||||
'<svg viewBox="0 0 15 15"><path d="M8.77,12.18c-.25,0-.46-.2-.46-.46s.2-.46.46-.46c1.47,0,2.67-1.2,2.67-2.67,0-1.57-1.34-2.67-3.26-2.67h-3.98l1.43,1.43c.18.18.18.47,0,.64-.18.18-.47.18-.64,0l-2.21-2.21c-.18-.18-.18-.47,0-.64l2.21-2.21c.18-.18.47-.18.64,0,.18.18.18.47,0,.64l-1.43,1.43h3.98c2.45,0,4.17,1.47,4.17,3.58,0,1.97-1.61,3.58-3.58,3.58Z"></path> </svg>'
|
||||
'<svg viewBox="0 0 15 15" style="width: 36px;height: 36px;pointer-events: none;fill: var(--input-text);"><path d="M8.77,12.18c-.25,0-.46-.2-.46-.46s.2-.46.46-.46c1.47,0,2.67-1.2,2.67-2.67,0-1.57-1.34-2.67-3.26-2.67h-3.98l1.43,1.43c.18.18.18.47,0,.64-.18.18-.47.18-.64,0l-2.21-2.21c-.18-.18-.18-.47,0-.64l2.21-2.21c.18-.18.47-.18.64,0,.18.18.18.47,0,.64l-1.43,1.43h3.98c2.45,0,4.17,1.47,4.17,3.58,0,1.97-1.61,3.58-3.58,3.58Z"></path> </svg>'
|
||||
|
||||
top_bar_undo_button.addEventListener('click', () => {
|
||||
this.messageBroker.publish('undo')
|
||||
@@ -3178,21 +3007,19 @@ class UIManager {
|
||||
|
||||
var top_bar_redo_button = document.createElement('div')
|
||||
top_bar_redo_button.id = 'maskEditor_topBarRedoButton'
|
||||
top_bar_redo_button.classList.add(iconButtonAccentColor)
|
||||
top_bar_redo_button.classList.add('maskEditor_topPanelIconButton')
|
||||
top_bar_redo_button.innerHTML =
|
||||
'<svg viewBox="0 0 15 15"> <path class="cls-1" d="M6.23,12.18c-1.97,0-3.58-1.61-3.58-3.58,0-2.11,1.71-3.58,4.17-3.58h3.98l-1.43-1.43c-.18-.18-.18-.47,0-.64.18-.18.46-.18.64,0l2.21,2.21c.09.09.13.2.13.32s-.05.24-.13.32l-2.21,2.21c-.18.18-.47.18-.64,0-.18-.18-.18-.47,0-.64l1.43-1.43h-3.98c-1.92,0-3.26,1.1-3.26,2.67,0,1.47,1.2,2.67,2.67,2.67.25,0,.46.2.46.46s-.2.46-.46.46Z"/></svg>'
|
||||
'<svg viewBox="0 0 15 15" style="width: 36px;height: 36px;pointer-events: none;fill: var(--input-text);"> <path class="cls-1" d="M6.23,12.18c-1.97,0-3.58-1.61-3.58-3.58,0-2.11,1.71-3.58,4.17-3.58h3.98l-1.43-1.43c-.18-.18-.18-.47,0-.64.18-.18.46-.18.64,0l2.21,2.21c.09.09.13.2.13.32s-.05.24-.13.32l-2.21,2.21c-.18.18-.47.18-.64,0-.18-.18-.18-.47,0-.64l1.43-1.43h-3.98c-1.92,0-3.26,1.1-3.26,2.67,0,1.47,1.2,2.67,2.67,2.67.25,0,.46.2.46.46s-.2.46-.46.46Z"/></svg>'
|
||||
|
||||
top_bar_redo_button.addEventListener('click', () => {
|
||||
this.messageBroker.publish('redo')
|
||||
})
|
||||
|
||||
var top_bar_invert_button = document.createElement('button')
|
||||
top_bar_invert_button.id = 'maskEditor_topBarInvertButton'
|
||||
top_bar_invert_button.classList.add(buttonAccentColor)
|
||||
top_bar_invert_button.innerText = 'Invert'
|
||||
top_bar_invert_button.addEventListener('click', () => {
|
||||
this.messageBroker.publish('invert')
|
||||
})
|
||||
top_bar_shortcuts_container.appendChild(top_bar_undo_button)
|
||||
top_bar_shortcuts_container.appendChild(top_bar_redo_button)
|
||||
|
||||
var top_bar_button_container = document.createElement('div')
|
||||
top_bar_button_container.id = 'maskEditor_topBarButtonContainer'
|
||||
|
||||
var top_bar_clear_button = document.createElement('button')
|
||||
top_bar_clear_button.id = 'maskEditor_topBarClearButton'
|
||||
@@ -3228,26 +3055,23 @@ class UIManager {
|
||||
this.maskEditor.close()
|
||||
})
|
||||
|
||||
top_bar_shortcuts_container.appendChild(top_bar_undo_button)
|
||||
top_bar_shortcuts_container.appendChild(top_bar_redo_button)
|
||||
top_bar_shortcuts_container.appendChild(top_bar_invert_button)
|
||||
top_bar_shortcuts_container.appendChild(top_bar_clear_button)
|
||||
top_bar_shortcuts_container.appendChild(top_bar_save_button)
|
||||
top_bar_shortcuts_container.appendChild(top_bar_cancel_button)
|
||||
top_bar_button_container.appendChild(top_bar_clear_button)
|
||||
top_bar_button_container.appendChild(top_bar_save_button)
|
||||
top_bar_button_container.appendChild(top_bar_cancel_button)
|
||||
|
||||
top_bar.appendChild(top_bar_title_container)
|
||||
top_bar.appendChild(top_bar_shortcuts_container)
|
||||
top_bar.appendChild(top_bar_button_container)
|
||||
|
||||
return top_bar
|
||||
}
|
||||
|
||||
//----------------
|
||||
|
||||
private createToolPanel() {
|
||||
var tool_panel = document.createElement('div')
|
||||
tool_panel.id = 'maskEditor_toolPanel'
|
||||
this.toolPanel = tool_panel
|
||||
var toolPanelHoverAccent = this.darkMode
|
||||
? 'maskEditor_toolPanelContainerDark'
|
||||
: 'maskEditor_toolPanelContainerLight'
|
||||
var pen_tool_panel = document.createElement('div')
|
||||
pen_tool_panel.id = 'maskEditor_toolPanel'
|
||||
this.toolPanel = pen_tool_panel
|
||||
|
||||
var toolElements: HTMLElement[] = []
|
||||
|
||||
@@ -3258,7 +3082,6 @@ class UIManager {
|
||||
toolPanel_brushToolContainer.classList.add(
|
||||
'maskEditor_toolPanelContainerSelected'
|
||||
)
|
||||
toolPanel_brushToolContainer.classList.add(toolPanelHoverAccent)
|
||||
toolPanel_brushToolContainer.innerHTML = `
|
||||
<svg viewBox="0 0 44 44">
|
||||
<path class="cls-1" d="M34,13.93c0,.47-.19.94-.55,1.31l-13.02,13.04c-.09.07-.18.15-.27.22-.07-1.39-1.21-2.48-2.61-2.49.07-.12.16-.24.27-.34l13.04-13.04c.72-.72,1.89-.72,2.6,0,.35.35.55.83.55,1.3Z"/>
|
||||
@@ -3293,7 +3116,6 @@ class UIManager {
|
||||
|
||||
var toolPanel_eraserToolContainer = document.createElement('div')
|
||||
toolPanel_eraserToolContainer.classList.add('maskEditor_toolPanelContainer')
|
||||
toolPanel_eraserToolContainer.classList.add(toolPanelHoverAccent)
|
||||
toolPanel_eraserToolContainer.innerHTML = `
|
||||
<svg viewBox="0 0 44 44">
|
||||
<g>
|
||||
@@ -3333,7 +3155,6 @@ class UIManager {
|
||||
toolPanel_paintBucketToolContainer.classList.add(
|
||||
'maskEditor_toolPanelContainer'
|
||||
)
|
||||
toolPanel_paintBucketToolContainer.classList.add(toolPanelHoverAccent)
|
||||
toolPanel_paintBucketToolContainer.innerHTML = `
|
||||
<svg viewBox="0 0 44 44">
|
||||
<path class="cls-1" d="M33.4,21.76l-11.42,11.41-.04.05c-.61.61-1.6.61-2.21,0l-8.91-8.91c-.61-.61-.61-1.6,0-2.21l.04-.05.3-.29h22.24Z"/>
|
||||
@@ -3377,7 +3198,6 @@ class UIManager {
|
||||
toolPanel_colorSelectToolContainer.classList.add(
|
||||
'maskEditor_toolPanelContainer'
|
||||
)
|
||||
toolPanel_colorSelectToolContainer.classList.add(toolPanelHoverAccent)
|
||||
toolPanel_colorSelectToolContainer.innerHTML = `
|
||||
<svg viewBox="0 0 44 44">
|
||||
<path class="cls-1" d="M30.29,13.72c-1.09-1.1-2.85-1.09-3.94,0l-2.88,2.88-.75-.75c-.2-.19-.51-.19-.71,0-.19.2-.19.51,0,.71l1.4,1.4-9.59,9.59c-.35.36-.54.82-.54,1.32,0,.14,0,.28.05.41-.05.04-.1.08-.15.13-.39.39-.39,1.01,0,1.4.38.39,1.01.39,1.4,0,.04-.04.08-.09.11-.13.14.04.3.06.45.06.5,0,.97-.19,1.32-.55l9.59-9.59,1.38,1.38c.1.09.22.14.35.14s.26-.05.35-.14c.2-.2.2-.52,0-.71l-.71-.72,2.88-2.89c1.08-1.08,1.08-2.85-.01-3.94ZM19.43,25.82h-2.46l7.15-7.15,1.23,1.23-5.92,5.92Z"/>
|
||||
@@ -3410,35 +3230,17 @@ class UIManager {
|
||||
toolPanel_colorSelectToolIndicator
|
||||
)
|
||||
|
||||
//zoom indicator
|
||||
var toolPanel_zoomIndicator = document.createElement('div')
|
||||
toolPanel_zoomIndicator.classList.add('maskEditor_toolPanelZoomIndicator')
|
||||
toolPanel_zoomIndicator.classList.add(toolPanelHoverAccent)
|
||||
pen_tool_panel.appendChild(toolPanel_brushToolContainer)
|
||||
pen_tool_panel.appendChild(toolPanel_eraserToolContainer)
|
||||
pen_tool_panel.appendChild(toolPanel_paintBucketToolContainer)
|
||||
pen_tool_panel.appendChild(toolPanel_colorSelectToolContainer)
|
||||
|
||||
var toolPanel_zoomText = document.createElement('span')
|
||||
toolPanel_zoomText.id = 'maskEditor_toolPanelZoomText'
|
||||
toolPanel_zoomText.innerText = '100%'
|
||||
this.zoomTextHTML = toolPanel_zoomText
|
||||
var pen_tool_panel_change_tool_button = document.createElement('button')
|
||||
pen_tool_panel_change_tool_button.id =
|
||||
'maskEditor_toolPanelChangeToolButton'
|
||||
pen_tool_panel_change_tool_button.innerText = 'change to Eraser'
|
||||
|
||||
var toolPanel_DimensionsText = document.createElement('span')
|
||||
toolPanel_DimensionsText.id = 'maskEditor_toolPanelDimensionsText'
|
||||
toolPanel_DimensionsText.innerText = ' '
|
||||
this.dimensionsTextHTML = toolPanel_DimensionsText
|
||||
|
||||
toolPanel_zoomIndicator.appendChild(toolPanel_zoomText)
|
||||
toolPanel_zoomIndicator.appendChild(toolPanel_DimensionsText)
|
||||
|
||||
toolPanel_zoomIndicator.addEventListener('click', () => {
|
||||
this.messageBroker.publish('resetZoom')
|
||||
})
|
||||
|
||||
tool_panel.appendChild(toolPanel_brushToolContainer)
|
||||
tool_panel.appendChild(toolPanel_eraserToolContainer)
|
||||
tool_panel.appendChild(toolPanel_paintBucketToolContainer)
|
||||
tool_panel.appendChild(toolPanel_colorSelectToolContainer)
|
||||
tool_panel.appendChild(toolPanel_zoomIndicator)
|
||||
|
||||
return tool_panel
|
||||
return pen_tool_panel
|
||||
}
|
||||
|
||||
private createPointerZone() {
|
||||
@@ -3582,8 +3384,6 @@ class UIManager {
|
||||
maskCanvas.width = this.image.width
|
||||
maskCanvas.height = this.image.height
|
||||
|
||||
this.dimensionsTextHTML.innerText = `${this.image.width}x${this.image.height}`
|
||||
|
||||
await this.invalidateCanvas(this.image, mask_image)
|
||||
this.messageBroker.publish('initZoomPan', [this.image, this.rootElement])
|
||||
}
|
||||
@@ -3838,14 +3638,6 @@ class UIManager {
|
||||
this.updateBrushPreview()
|
||||
this.setBrushPreviewGradientVisibility(false)
|
||||
}
|
||||
|
||||
setZoomText(zoomText: string) {
|
||||
this.zoomTextHTML.innerText = zoomText
|
||||
}
|
||||
|
||||
setDimensionsText(dimensionsText: string) {
|
||||
this.dimensionsTextHTML.innerText = dimensionsText
|
||||
}
|
||||
}
|
||||
|
||||
class ToolManager {
|
||||
@@ -4022,7 +3814,6 @@ class PanAndZoomManager {
|
||||
lastTouchPoint: Point = { x: 0, y: 0 }
|
||||
|
||||
zoom_ratio: number = 1
|
||||
interpolatedZoomRatio: number = 1
|
||||
pan_offset: Offset = { x: 0, y: 0 }
|
||||
|
||||
mouseDownPoint: Point | null = null
|
||||
@@ -4030,11 +3821,8 @@ class PanAndZoomManager {
|
||||
|
||||
canvasContainer: HTMLElement | null = null
|
||||
maskCanvas: HTMLCanvasElement | null = null
|
||||
rootElement: HTMLElement | null = null
|
||||
|
||||
image: HTMLImageElement | null = null
|
||||
imageRootWidth: number = 0
|
||||
imageRootHeight: number = 0
|
||||
|
||||
cursorPoint: Point = { x: 0, y: 0 }
|
||||
|
||||
@@ -4090,11 +3878,6 @@ class PanAndZoomManager {
|
||||
this.handleTouchEnd(event)
|
||||
}
|
||||
)
|
||||
|
||||
this.messageBroker.subscribe('resetZoom', async () => {
|
||||
if (this.interpolatedZoomRatio === 1) return
|
||||
await this.smoothResetView()
|
||||
})
|
||||
}
|
||||
|
||||
private addPullTopics() {
|
||||
@@ -4271,25 +4054,14 @@ class PanAndZoomManager {
|
||||
const mouseX = cursorPoint.x - rect.left
|
||||
const mouseY = cursorPoint.y - rect.top
|
||||
|
||||
console.log(oldZoom, newZoom)
|
||||
// Calculate new pan position
|
||||
const scaleFactor = newZoom / oldZoom
|
||||
this.pan_offset.x += mouseX - mouseX * scaleFactor
|
||||
this.pan_offset.y += mouseY - mouseY * scaleFactor
|
||||
|
||||
console.log(this.imageRootWidth, this.imageRootHeight)
|
||||
|
||||
// Update pan and zoom immediately
|
||||
await this.invalidatePanZoom()
|
||||
|
||||
const newImageWidth = maskCanvas.clientWidth
|
||||
|
||||
const zoomRatio = newImageWidth / this.imageRootWidth
|
||||
|
||||
this.interpolatedZoomRatio = zoomRatio
|
||||
|
||||
this.messageBroker.publish('setZoomText', `${Math.round(zoomRatio * 100)}%`)
|
||||
|
||||
// Update cursor position with new pan values
|
||||
this.updateCursorPosition(cursorPoint)
|
||||
|
||||
@@ -4299,125 +4071,51 @@ class PanAndZoomManager {
|
||||
})
|
||||
}
|
||||
|
||||
private async smoothResetView(duration: number = 500) {
|
||||
// Store initial state
|
||||
const startZoom = this.zoom_ratio
|
||||
const startPan = { ...this.pan_offset }
|
||||
|
||||
// Panel dimensions
|
||||
const sidePanelWidth = 220
|
||||
const toolPanelWidth = 64
|
||||
const topBarHeight = 44
|
||||
|
||||
// Calculate available space
|
||||
const availableWidth =
|
||||
this.rootElement!.clientWidth - sidePanelWidth - toolPanelWidth
|
||||
const availableHeight = this.rootElement!.clientHeight - topBarHeight
|
||||
|
||||
// Calculate target zoom
|
||||
const zoomRatioWidth = availableWidth / this.image!.width
|
||||
const zoomRatioHeight = availableHeight / this.image!.height
|
||||
const targetZoom = Math.min(zoomRatioWidth, zoomRatioHeight)
|
||||
|
||||
// Calculate final dimensions
|
||||
const aspectRatio = this.image!.width / this.image!.height
|
||||
let finalWidth = 0
|
||||
let finalHeight = 0
|
||||
|
||||
// Calculate target pan position
|
||||
const targetPan = { x: toolPanelWidth, y: topBarHeight }
|
||||
|
||||
if (zoomRatioHeight > zoomRatioWidth) {
|
||||
finalWidth = availableWidth
|
||||
finalHeight = finalWidth / aspectRatio
|
||||
targetPan.y = (availableHeight - finalHeight) / 2 + topBarHeight
|
||||
} else {
|
||||
finalHeight = availableHeight
|
||||
finalWidth = finalHeight * aspectRatio
|
||||
targetPan.x = (availableWidth - finalWidth) / 2 + toolPanelWidth
|
||||
}
|
||||
|
||||
const startTime = performance.now()
|
||||
const animate = (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
// Cubic easing out for smooth deceleration
|
||||
const eased = 1 - Math.pow(1 - progress, 3)
|
||||
|
||||
// Calculate intermediate zoom and pan values
|
||||
const currentZoom = startZoom + (targetZoom - startZoom) * eased
|
||||
|
||||
this.zoom_ratio = currentZoom
|
||||
this.pan_offset.x = startPan.x + (targetPan.x - startPan.x) * eased
|
||||
this.pan_offset.y = startPan.y + (targetPan.y - startPan.y) * eased
|
||||
|
||||
this.invalidatePanZoom()
|
||||
|
||||
const interpolatedZoomRatio = startZoom + (1.0 - startZoom) * eased
|
||||
|
||||
this.messageBroker.publish(
|
||||
'setZoomText',
|
||||
`${Math.round(interpolatedZoomRatio * 100)}%`
|
||||
)
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate)
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(animate)
|
||||
this.interpolatedZoomRatio = 1.0
|
||||
}
|
||||
|
||||
async initializeCanvasPanZoom(
|
||||
image: HTMLImageElement,
|
||||
rootElement: HTMLElement
|
||||
) {
|
||||
// Get side panel width
|
||||
let sidePanelWidth = 220
|
||||
const toolPanelWidth = 64
|
||||
let topBarHeight = 44
|
||||
|
||||
this.rootElement = rootElement
|
||||
|
||||
// Calculate available width accounting for both side panels
|
||||
let availableWidth =
|
||||
rootElement.clientWidth - sidePanelWidth - toolPanelWidth
|
||||
let availableWidth = rootElement.clientWidth - 2 * sidePanelWidth
|
||||
let availableHeight = rootElement.clientHeight - topBarHeight
|
||||
|
||||
let zoomRatioWidth = availableWidth / image.width
|
||||
let zoomRatioHeight = availableHeight / image.height
|
||||
// Initial dimensions
|
||||
let drawWidth = image.width
|
||||
let drawHeight = image.height
|
||||
|
||||
let aspectRatio = image.width / image.height
|
||||
// First check if width needs scaling
|
||||
if (drawWidth > availableWidth) {
|
||||
drawWidth = availableWidth
|
||||
drawHeight = (drawWidth / image.width) * image.height
|
||||
}
|
||||
|
||||
let finalWidth = 0
|
||||
let finalHeight = 0
|
||||
|
||||
let pan_offset: Offset = { x: toolPanelWidth, y: topBarHeight }
|
||||
|
||||
if (zoomRatioHeight > zoomRatioWidth) {
|
||||
finalWidth = availableWidth
|
||||
finalHeight = finalWidth / aspectRatio
|
||||
pan_offset.y = (availableHeight - finalHeight) / 2 + topBarHeight
|
||||
} else {
|
||||
finalHeight = availableHeight
|
||||
finalWidth = finalHeight * aspectRatio
|
||||
pan_offset.x = (availableWidth - finalWidth) / 2 + toolPanelWidth
|
||||
// Then check if height needs scaling
|
||||
if (drawHeight > availableHeight) {
|
||||
drawHeight = availableHeight
|
||||
drawWidth = (drawHeight / image.height) * image.width
|
||||
}
|
||||
|
||||
if (this.image === null) {
|
||||
this.image = image
|
||||
}
|
||||
|
||||
this.imageRootWidth = finalWidth
|
||||
this.imageRootHeight = finalHeight
|
||||
this.zoom_ratio = drawWidth / image.width
|
||||
|
||||
this.zoom_ratio = Math.min(zoomRatioWidth, zoomRatioHeight)
|
||||
this.pan_offset = pan_offset
|
||||
// Center the canvas in the available space
|
||||
const canvasX = sidePanelWidth + (availableWidth - drawWidth) / 2
|
||||
const canvasY = (availableHeight - drawHeight) / 2
|
||||
|
||||
this.pan_offset.x = canvasX
|
||||
this.pan_offset.y = canvasY
|
||||
|
||||
await this.invalidatePanZoom()
|
||||
}
|
||||
|
||||
//probably move to PanZoomManager
|
||||
async invalidatePanZoom() {
|
||||
// Single validation check upfront
|
||||
if (
|
||||
@@ -4434,6 +4132,10 @@ class PanAndZoomManager {
|
||||
const raw_width = this.image.width * this.zoom_ratio
|
||||
const raw_height = this.image.height * this.zoom_ratio
|
||||
|
||||
// Adjust pan offset
|
||||
this.pan_offset.x = Math.max(10 - raw_width, this.pan_offset.x)
|
||||
this.pan_offset.y = Math.max(10 - raw_height, this.pan_offset.y)
|
||||
|
||||
// Get canvas container
|
||||
this.canvasContainer ??=
|
||||
await this.messageBroker?.pull('getCanvasContainer')
|
||||
@@ -4536,9 +4238,6 @@ class MessageBroker {
|
||||
this.createPushTopic('setMaskBoundary')
|
||||
this.createPushTopic('setMaskTolerance')
|
||||
this.createPushTopic('setBrushSmoothingPrecision')
|
||||
this.createPushTopic('setZoomText')
|
||||
this.createPushTopic('resetZoom')
|
||||
this.createPushTopic('invert')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4726,38 +4425,12 @@ app.registerExtension({
|
||||
settings: [
|
||||
{
|
||||
id: 'Comfy.MaskEditor.UseNewEditor',
|
||||
category: ['Mask Editor', 'NewEditor'],
|
||||
category: ['Comfy', 'Masking'],
|
||||
name: 'Use new mask editor',
|
||||
tooltip: 'Switch to the new mask editor interface',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.MaskEditor.BrushAdjustmentSpeed',
|
||||
category: ['Mask Editor', 'BrushAdjustment', 'Sensitivity'],
|
||||
name: 'Brush adjustment speed multiplier',
|
||||
tooltip:
|
||||
'Controls how quickly the brush size and hardness change when adjusting. Higher values mean faster changes.',
|
||||
experimental: true,
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 0.1,
|
||||
max: 2.0,
|
||||
step: 0.1
|
||||
},
|
||||
defaultValue: 1.0,
|
||||
versionAdded: '1.0.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.MaskEditor.UseDominantAxis',
|
||||
category: ['Mask Editor', 'BrushAdjustment', 'UseDominantAxis'],
|
||||
name: 'Lock brush adjustment to dominant axis',
|
||||
tooltip:
|
||||
'When enabled, brush adjustments will only affect size OR hardness based on which direction you move more',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
experimental: true
|
||||
}
|
||||
],
|
||||
init(app) {
|
||||
|
||||
@@ -54,7 +54,6 @@ app.registerExtension({
|
||||
LiteGraph.registered_slot_in_types[lowerType] = { nodes: [] }
|
||||
}
|
||||
LiteGraph.registered_slot_in_types[lowerType].nodes.push(
|
||||
// @ts-expect-error ComfyNode
|
||||
nodeType.comfyClass
|
||||
)
|
||||
}
|
||||
@@ -72,7 +71,6 @@ app.registerExtension({
|
||||
if (!(type in LiteGraph.registered_slot_out_types)) {
|
||||
LiteGraph.registered_slot_out_types[type] = { nodes: [] }
|
||||
}
|
||||
// @ts-expect-error ComfyNode
|
||||
LiteGraph.registered_slot_out_types[type].nodes.push(nodeType.comfyClass)
|
||||
|
||||
if (!LiteGraph.slot_types_out.includes(type)) {
|
||||
|
||||
@@ -81,7 +81,6 @@ app.registerExtension({
|
||||
name: 'Comfy.AudioWidget',
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (
|
||||
// @ts-expect-error ComfyNode
|
||||
['LoadAudio', 'SaveAudio', 'PreviewAudio'].includes(nodeType.comfyClass)
|
||||
) {
|
||||
nodeData.input.required.audioUI = ['AUDIO_UI']
|
||||
|
||||
@@ -73,22 +73,17 @@ app.registerExtension({
|
||||
const canvas = document.createElement('canvas')
|
||||
|
||||
const capture = () => {
|
||||
// @ts-expect-error widget value type narrow down
|
||||
canvas.width = w.value
|
||||
// @ts-expect-error widget value type narrow down
|
||||
canvas.height = h.value
|
||||
const ctx = canvas.getContext('2d')
|
||||
// @ts-expect-error widget value type narrow down
|
||||
ctx.drawImage(video, 0, 0, w.value, h.value)
|
||||
const data = canvas.toDataURL('image/png')
|
||||
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
// @ts-expect-error adding extra property
|
||||
node.imgs = [img]
|
||||
app.graph.setDirtyCanvas(true)
|
||||
requestAnimationFrame(() => {
|
||||
// @ts-expect-error accessing extra property
|
||||
node.setSizeForImage?.()
|
||||
})
|
||||
}
|
||||
@@ -102,14 +97,11 @@ app.registerExtension({
|
||||
capture
|
||||
)
|
||||
btn.disabled = true
|
||||
// @ts-expect-error hacky override
|
||||
btn.serializeValue = () => undefined
|
||||
|
||||
// @ts-expect-error hacky override
|
||||
camera.serializeValue = async () => {
|
||||
if (captureOnQueue.value) {
|
||||
capture()
|
||||
// @ts-expect-error accessing extra property
|
||||
} else if (!node.imgs?.length) {
|
||||
const err = `No webcam image captured`
|
||||
useToastStore().addAlert(err)
|
||||
|
||||
@@ -724,7 +724,6 @@ app.registerExtension({
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
// Add menu options to convert to/from widgets
|
||||
const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions
|
||||
// @ts-expect-error adding extra property
|
||||
nodeType.prototype.convertWidgetToInput = function (widget) {
|
||||
const config = getConfig.call(this, widget.name) ?? [
|
||||
widget.type,
|
||||
@@ -747,10 +746,8 @@ app.registerExtension({
|
||||
if (w.options?.forceInput) {
|
||||
continue
|
||||
}
|
||||
// @ts-expect-error custom widget type
|
||||
if (w.type === CONVERTED_TYPE) {
|
||||
toWidget.push({
|
||||
// @ts-expect-error never
|
||||
content: `Convert ${w.name} to widget`,
|
||||
callback: () => convertToWidget(this, w)
|
||||
})
|
||||
@@ -863,9 +860,7 @@ app.registerExtension({
|
||||
for (const input of this.inputs) {
|
||||
if (input.widget && !input.widget[GET_CONFIG]) {
|
||||
input.widget[GET_CONFIG] = () =>
|
||||
// @ts-expect-error input.widget has unknown type
|
||||
getConfig.call(this, input.widget.name)
|
||||
// @ts-expect-error input.widget has unknown type
|
||||
const w = this.widgets.find((w) => w.name === input.widget.name)
|
||||
if (w) {
|
||||
hideWidget(this, w)
|
||||
@@ -940,7 +935,6 @@ app.registerExtension({
|
||||
originNode,
|
||||
originSlot
|
||||
) {
|
||||
// @ts-expect-error onConnectInput has 5 arguments
|
||||
const v = onConnectInput?.(this, arguments)
|
||||
// Not a combo, ignore
|
||||
if (type !== 'COMBO') return v
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
|
||||
export function useCopyToClipboard() {
|
||||
const { copy, isSupported } = useClipboard()
|
||||
const toast = useToast()
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
if (isSupported) {
|
||||
try {
|
||||
await copy(text)
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Success',
|
||||
detail: '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'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
copyToClipboard
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { useQueuePendingTaskCountStore } from '@/stores/queueStore'
|
||||
import { markRaw } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import DocumentationSidebarTab from '@/components/sidebar/tabs/DocumentationSidebarTab.vue'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
export const useDocumentationSidebarTab = (): SidebarTabExtension => {
|
||||
const { t } = useI18n()
|
||||
const queuePendingTaskCountStore = useQueuePendingTaskCountStore()
|
||||
return {
|
||||
id: 'documentation',
|
||||
icon: 'mdi mdi-help',
|
||||
title: t('sideToolbar.documentation'),
|
||||
tooltip: t('sideToolbar.documentation'),
|
||||
component: markRaw(DocumentationSidebarTab),
|
||||
type: 'vue'
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,8 @@ export const useModelLibrarySidebarTab = (): SidebarTabExtension => {
|
||||
iconBadge: () => {
|
||||
if (isElectron()) {
|
||||
const electronDownloadStore = useElectronDownloadStore()
|
||||
if (electronDownloadStore.inProgressDownloads.length > 0) {
|
||||
return electronDownloadStore.inProgressDownloads.length.toString()
|
||||
if (electronDownloadStore.downloads.length > 0) {
|
||||
return electronDownloadStore.downloads.length.toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
453
src/i18n.ts
453
src/i18n.ts
@@ -1,16 +1,453 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import en from './locales/en'
|
||||
import zh from './locales/zh'
|
||||
import ru from './locales/ru'
|
||||
|
||||
const messages = {
|
||||
en: {
|
||||
welcome: {
|
||||
title: 'Welcome to ComfyUI',
|
||||
getStarted: 'Get Started'
|
||||
},
|
||||
install: {
|
||||
installLocation: 'Install Location',
|
||||
migration: 'Migration',
|
||||
desktopSettings: 'Desktop Settings',
|
||||
chooseInstallationLocation: 'Choose Installation Location',
|
||||
systemLocations: 'System Locations',
|
||||
failedToSelectDirectory: 'Failed to select directory',
|
||||
pathValidationFailed: 'Failed to validate path',
|
||||
installLocationDescription:
|
||||
"Select the directory for ComfyUI's user data. A python environment will be installed to the selected location. Please make sure the selected disk has enough space (~15GB) left.",
|
||||
installLocationTooltip:
|
||||
"ComfyUI's user data directory. Stores:\n- Python Environment\n- Models\n- Custom nodes\n",
|
||||
appDataLocationTooltip:
|
||||
"ComfyUI's app data directory. Stores:\n- Logs\n- Server configs",
|
||||
appPathLocationTooltip:
|
||||
"ComfyUI's app asset directory. Stores the ComfyUI code and assets",
|
||||
migrateFromExistingInstallation: 'Migrate from Existing Installation',
|
||||
migrationSourcePathDescription:
|
||||
'If you have an existing ComfyUI installation, we can copy/link your existing user files and models to the new installation.',
|
||||
selectItemsToMigrate: 'Select Items to Migrate',
|
||||
migrationOptional:
|
||||
"Migration is optional. If you don't have an existing installation, you can skip this step.",
|
||||
desktopAppSettings: 'Desktop App Settings',
|
||||
desktopAppSettingsDescription:
|
||||
'Configure how ComfyUI behaves on your desktop. You can change these settings later.',
|
||||
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.',
|
||||
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'
|
||||
}
|
||||
}
|
||||
},
|
||||
serverStart: {
|
||||
reinstall: 'Reinstall',
|
||||
reportIssue: 'Report Issue',
|
||||
openLogs: 'Open Logs',
|
||||
process: {
|
||||
'initial-state': 'Loading...',
|
||||
'python-setup': 'Setting up Python Environment...',
|
||||
'starting-server': 'Starting ComfyUI server...',
|
||||
ready: 'Finishing...',
|
||||
error: 'Unable to start ComfyUI'
|
||||
}
|
||||
},
|
||||
firstTimeUIMessage:
|
||||
'This is the first time you use the new UI. Choose "Menu > Use New Menu > Disabled" to restore the old UI.',
|
||||
download: 'Download',
|
||||
loadAllFolders: 'Load All Folders',
|
||||
refresh: 'Refresh',
|
||||
terminal: 'Terminal',
|
||||
logs: 'Logs',
|
||||
videoFailedToLoad: 'Video failed to load',
|
||||
extensionName: 'Extension Name',
|
||||
reloadToApplyChanges: 'Reload to apply changes',
|
||||
insert: 'Insert',
|
||||
systemInfo: 'System Info',
|
||||
devices: 'Devices',
|
||||
about: 'About',
|
||||
add: 'Add',
|
||||
confirm: 'Confirm',
|
||||
reset: 'Reset',
|
||||
resetKeybindingsTooltip: 'Reset keybindings to default',
|
||||
customizeFolder: 'Customize Folder',
|
||||
icon: 'Icon',
|
||||
color: 'Color',
|
||||
bookmark: 'Bookmark',
|
||||
folder: 'Folder',
|
||||
star: 'Star',
|
||||
heart: 'Heart',
|
||||
file: 'File',
|
||||
inbox: 'Inbox',
|
||||
box: 'Box',
|
||||
briefcase: 'Briefcase',
|
||||
error: 'Error',
|
||||
loading: 'Loading',
|
||||
findIssues: 'Find Issues',
|
||||
reportIssue: 'Send Report',
|
||||
reportIssueTooltip: 'Submit the error report to Comfy Org',
|
||||
reportSent: 'Report Submitted',
|
||||
copyToClipboard: 'Copy to Clipboard',
|
||||
openNewIssue: 'Open New Issue',
|
||||
showReport: 'Show Report',
|
||||
imageFailedToLoad: 'Image failed to load',
|
||||
reconnecting: 'Reconnecting',
|
||||
reconnected: 'Reconnected',
|
||||
delete: 'Delete',
|
||||
rename: 'Rename',
|
||||
customize: 'Customize',
|
||||
experimental: 'BETA',
|
||||
deprecated: 'DEPR',
|
||||
loadWorkflow: 'Load Workflow',
|
||||
goToNode: 'Go to Node',
|
||||
settings: 'Settings',
|
||||
searchWorkflows: 'Search Workflows',
|
||||
searchSettings: 'Search Settings',
|
||||
searchNodes: 'Search Nodes',
|
||||
searchModels: 'Search Models',
|
||||
searchKeybindings: 'Search Keybindings',
|
||||
noResultsFound: 'No Results Found',
|
||||
searchFailedMessage:
|
||||
"We couldn't find any settings matching your search. Try adjusting your search terms.",
|
||||
noTasksFound: 'No Tasks Found',
|
||||
noTasksFoundMessage: 'There are no tasks in the queue.',
|
||||
newFolder: 'New Folder',
|
||||
sideToolbar: {
|
||||
themeToggle: 'Toggle Theme',
|
||||
queue: 'Queue',
|
||||
nodeLibrary: 'Node Library',
|
||||
workflows: 'Workflows',
|
||||
browseTemplates: 'Browse example templates',
|
||||
openWorkflow: 'Open workflow in local file system',
|
||||
newBlankWorkflow: 'Create a new blank workflow',
|
||||
nodeLibraryTab: {
|
||||
sortOrder: 'Sort Order'
|
||||
},
|
||||
modelLibrary: 'Model Library',
|
||||
downloads: 'Downloads',
|
||||
queueTab: {
|
||||
showFlatList: 'Show Flat List',
|
||||
backToAllTasks: 'Back to All Tasks',
|
||||
containImagePreview: 'Fill Image Preview',
|
||||
coverImagePreview: 'Fit Image Preview',
|
||||
clearPendingTasks: 'Clear Pending Tasks',
|
||||
filter: 'Filter Outputs',
|
||||
filters: {
|
||||
hideCached: 'Hide Cached',
|
||||
hideCanceled: 'Hide Canceled'
|
||||
}
|
||||
}
|
||||
},
|
||||
menu: {
|
||||
hideMenu: 'Hide Menu',
|
||||
showMenu: 'Show Menu',
|
||||
batchCount: 'Batch Count',
|
||||
batchCountTooltip:
|
||||
'The number of times the workflow generation should be queued',
|
||||
autoQueue: 'Auto Queue',
|
||||
disabled: 'Disabled',
|
||||
disabledTooltip: 'The workflow will not be automatically queued',
|
||||
instant: 'Instant',
|
||||
instantTooltip:
|
||||
'The workflow will be queued instantly after a generation finishes',
|
||||
change: 'On Change',
|
||||
changeTooltip: 'The workflow will be queued once a change is made',
|
||||
queueWorkflow: 'Queue workflow (Shift to queue at front)',
|
||||
queueWorkflowFront: 'Queue workflow at front',
|
||||
queue: 'Queue',
|
||||
interrupt: 'Cancel current run',
|
||||
refresh: 'Refresh node definitions',
|
||||
clipspace: 'Open Clipspace',
|
||||
resetView: 'Reset canvas view',
|
||||
clear: 'Clear workflow',
|
||||
toggleBottomPanel: 'Toggle Bottom Panel'
|
||||
},
|
||||
templateWorkflows: {
|
||||
title: 'Get Started with a Template',
|
||||
template: {
|
||||
default: 'Image Generation',
|
||||
image2image: 'Image to Image',
|
||||
upscale: '2 Pass Upscale',
|
||||
flux_schnell: 'Flux Schnell'
|
||||
}
|
||||
},
|
||||
graphCanvasMenu: {
|
||||
zoomIn: 'Zoom In',
|
||||
zoomOut: 'Zoom Out',
|
||||
resetView: 'Reset View',
|
||||
fitView: 'Fit View',
|
||||
selectMode: 'Select Mode',
|
||||
panMode: 'Pan Mode',
|
||||
toggleLinkVisibility: 'Toggle Link Visibility'
|
||||
},
|
||||
electronFileDownload: {
|
||||
inProgress: 'In Progress',
|
||||
pause: 'Pause Download',
|
||||
paused: 'Paused',
|
||||
resume: 'Resume Download',
|
||||
cancel: 'Cancel Download',
|
||||
cancelled: 'Cancelled'
|
||||
}
|
||||
},
|
||||
zh: {
|
||||
firstTimeUIMessage:
|
||||
'这是您第一次使用新界面。选择“Menu > Use New Menu > Disabled”以恢复旧界面。',
|
||||
download: '下载',
|
||||
loadAllFolders: '加载所有文件夹',
|
||||
refresh: '刷新',
|
||||
terminal: '终端',
|
||||
videoFailedToLoad: '视频加载失败',
|
||||
extensionName: '扩展名称',
|
||||
reloadToApplyChanges: '重新加载以应用更改',
|
||||
insert: '插入',
|
||||
systemInfo: '系统信息',
|
||||
devices: '设备',
|
||||
about: '关于',
|
||||
add: '添加',
|
||||
confirm: '确认',
|
||||
reset: '重置',
|
||||
resetKeybindingsTooltip: '重置键位',
|
||||
customizeFolder: '定制文件夹',
|
||||
icon: '图标',
|
||||
color: '颜色',
|
||||
bookmark: '书签',
|
||||
folder: '文件夹',
|
||||
star: '星星',
|
||||
heart: '心',
|
||||
file: '文件',
|
||||
inbox: '收件箱',
|
||||
box: '盒子',
|
||||
briefcase: '公文包',
|
||||
error: '错误',
|
||||
loading: '加载中',
|
||||
findIssues: '查找 Issue',
|
||||
copyToClipboard: '复制到剪贴板',
|
||||
openNewIssue: '开启新 Issue',
|
||||
showReport: '显示报告',
|
||||
imageFailedToLoad: '图像加载失败',
|
||||
reconnecting: '重新连接中',
|
||||
reconnected: '已重新连接',
|
||||
delete: '删除',
|
||||
rename: '重命名',
|
||||
customize: '定制',
|
||||
experimental: 'BETA',
|
||||
deprecated: '弃用',
|
||||
loadWorkflow: '加载工作流',
|
||||
goToNode: '前往节点',
|
||||
settings: '设置',
|
||||
searchWorkflows: '搜索工作流',
|
||||
searchSettings: '搜索设置',
|
||||
searchNodes: '搜索节点',
|
||||
searchModels: '搜索模型',
|
||||
searchKeybindings: '搜索键位',
|
||||
noResultsFound: '未找到结果',
|
||||
searchFailedMessage:
|
||||
'我们找不到与您的搜索匹配的任何设置。请尝试调整搜索条件。',
|
||||
noContent: '(无内容)',
|
||||
noTasksFound: '未找到任务',
|
||||
noTasksFoundMessage: '队列中没有任务。',
|
||||
newFolder: '新建文件夹',
|
||||
sideToolbar: {
|
||||
themeToggle: '主题切换',
|
||||
queue: '队列',
|
||||
nodeLibrary: '节点库',
|
||||
workflows: '工作流',
|
||||
browseTemplates: '浏览示例模板',
|
||||
openWorkflow: '在本地文件系统中打开工作流',
|
||||
newBlankWorkflow: '创建一个新空白工作流',
|
||||
nodeLibraryTab: {
|
||||
sortOrder: '排序顺序'
|
||||
},
|
||||
modelLibrary: '模型库',
|
||||
queueTab: {
|
||||
showFlatList: '平铺结果',
|
||||
backToAllTasks: '返回',
|
||||
containImagePreview: '填充图像预览',
|
||||
coverImagePreview: '适应图像预览',
|
||||
clearPendingTasks: '清除待处理任务'
|
||||
}
|
||||
},
|
||||
menu: {
|
||||
hideMenu: '隐藏菜单',
|
||||
showMenu: '显示菜单',
|
||||
batchCount: '批次数量',
|
||||
batchCountTooltip: '工作流生成次数',
|
||||
autoQueue: '自动执行',
|
||||
disabled: '禁用',
|
||||
disabledTooltip: '工作流将不会自动执行',
|
||||
instant: '实时',
|
||||
instantTooltip: '工作流将会在生成完成后立即执行',
|
||||
change: '变动',
|
||||
changeTooltip: '工作流将会在改变后执行',
|
||||
queueWorkflow: '执行 (Shift 执行到队列首)',
|
||||
queueWorkflowFront: '执行到队列首',
|
||||
queue: '队列',
|
||||
interrupt: '取消当前任务',
|
||||
refresh: '刷新节点',
|
||||
clipspace: '打开剪贴板',
|
||||
resetView: '重置画布视图',
|
||||
clear: '清空工作流',
|
||||
toggleBottomPanel: '底部面板'
|
||||
},
|
||||
templateWorkflows: {
|
||||
title: '从模板开始',
|
||||
template: {
|
||||
default: 'Image Generation',
|
||||
image2image: 'Image to Image',
|
||||
upscale: '2 Pass Upscale',
|
||||
flux_schnell: 'Flux Schnell'
|
||||
}
|
||||
},
|
||||
graphCanvasMenu: {
|
||||
zoomIn: '放大',
|
||||
zoomOut: '缩小',
|
||||
resetView: '重置视图',
|
||||
fitView: '适应视图',
|
||||
selectMode: '选择模式',
|
||||
panMode: '平移模式',
|
||||
toggleLinkVisibility: '切换链接可见性'
|
||||
}
|
||||
},
|
||||
ru: {
|
||||
download: 'Скачать',
|
||||
refresh: 'Обновить',
|
||||
loadAllFolders: 'Загрузить все папки',
|
||||
terminal: 'Терминал',
|
||||
videoFailedToLoad: 'Видео не удалось загрузить',
|
||||
extensionName: 'Название расширения',
|
||||
reloadToApplyChanges: 'Перезагрузите, чтобы применить изменения',
|
||||
insert: 'Вставить',
|
||||
systemInfo: 'Информация о системе',
|
||||
devices: 'Устройства',
|
||||
about: 'О',
|
||||
add: 'Добавить',
|
||||
confirm: 'Подтвердить',
|
||||
reset: 'Сбросить',
|
||||
resetKeybindingsTooltip: 'Сбросить сочетания клавиш по умолчанию',
|
||||
customizeFolder: 'Настроить папку',
|
||||
icon: 'Иконка',
|
||||
color: 'Цвет',
|
||||
bookmark: 'Закладка',
|
||||
folder: 'Папка',
|
||||
star: 'Звёздочка',
|
||||
heart: 'Сердце',
|
||||
file: 'Файл',
|
||||
inbox: 'Входящие',
|
||||
box: 'Ящик',
|
||||
briefcase: 'Чемодан',
|
||||
error: 'Ошибка',
|
||||
loading: 'Загрузка',
|
||||
findIssues: 'Найти Issue',
|
||||
copyToClipboard: 'Копировать в буфер обмена',
|
||||
openNewIssue: 'Открыть новый Issue',
|
||||
showReport: 'Показать отчёт',
|
||||
imageFailedToLoad: 'Изображение не удалось загрузить',
|
||||
reconnecting: 'Переподключение',
|
||||
reconnected: 'Переподключено',
|
||||
delete: 'Удалить',
|
||||
rename: 'Переименовать',
|
||||
customize: 'Настроить',
|
||||
experimental: 'БЕТА',
|
||||
deprecated: 'УСТАР',
|
||||
loadWorkflow: 'Загрузить рабочий процесс',
|
||||
goToNode: 'Перейти к узлу',
|
||||
settings: 'Настройки',
|
||||
searchWorkflows: 'Поиск рабочих процессов',
|
||||
searchSettings: 'Поиск настроек',
|
||||
searchNodes: 'Поиск узлов',
|
||||
searchModels: 'Поиск моделей',
|
||||
searchKeybindings: 'Поиск сочетаний клавиш',
|
||||
noResultsFound: 'Ничего не найдено',
|
||||
searchFailedMessage:
|
||||
'Не удалось найти ни одной настройки, соответствующей вашему запросу. Попробуйте скорректировать поисковый запрос.',
|
||||
noContent: '(Нет контента)',
|
||||
noTasksFound: 'Задачи не найдены',
|
||||
noTasksFoundMessage: 'В очереди нет задач.',
|
||||
newFolder: 'Новая папка',
|
||||
sideToolbar: {
|
||||
themeToggle: 'Переключить тему',
|
||||
queue: 'Очередь',
|
||||
nodeLibrary: 'Библиотека узлов',
|
||||
workflows: 'Рабочие процессы',
|
||||
browseTemplates: 'Просмотреть примеры шаблонов',
|
||||
openWorkflow: 'Открыть рабочий процесс в локальной файловой системе',
|
||||
newBlankWorkflow: 'Создайте новый пустой рабочий процесс',
|
||||
nodeLibraryTab: {
|
||||
sortOrder: 'Порядок сортировки'
|
||||
},
|
||||
modelLibrary: 'Библиотека моделей',
|
||||
queueTab: {
|
||||
showFlatList: 'Показать плоский список',
|
||||
backToAllTasks: 'Вернуться ко всем задачам',
|
||||
containImagePreview: 'Предпросмотр заливающего изображения',
|
||||
coverImagePreview: 'Предпросмотр подходящего изображения',
|
||||
clearPendingTasks: 'Очистить отложенные задачи'
|
||||
}
|
||||
},
|
||||
menu: {
|
||||
hideMenu: 'Скрыть меню',
|
||||
showMenu: 'Показать меню',
|
||||
batchCount: 'Количество пакетов',
|
||||
batchCountTooltip:
|
||||
'Количество раз, когда генерация рабочего процесса должна быть помещена в очередь',
|
||||
autoQueue: 'Автоочередь',
|
||||
disabled: 'Отключено',
|
||||
disabledTooltip:
|
||||
'Рабочий процесс не будет автоматически помещён в очередь',
|
||||
instant: 'Мгновенно',
|
||||
instantTooltip:
|
||||
'Рабочий процесс будет помещён в очередь сразу же после завершения генерации',
|
||||
change: 'При изменении',
|
||||
changeTooltip:
|
||||
'Рабочий процесс будет поставлен в очередь после внесения изменений',
|
||||
queueWorkflow: 'Очередь рабочего процесса (Shift для вставки спереди)',
|
||||
queueWorkflowFront: 'Очередь рабочего процесса (Вставка спереди)',
|
||||
queue: 'Очередь',
|
||||
interrupt: 'Отменить текущее выполнение',
|
||||
refresh: 'Обновить определения узлов',
|
||||
clipspace: 'Открыть Clipspace',
|
||||
resetView: 'Сбросить вид холста',
|
||||
clear: 'Очистить рабочий процесс'
|
||||
},
|
||||
templateWorkflows: {
|
||||
title: 'Начните работу с шаблона',
|
||||
template: {
|
||||
default: 'Image Generation',
|
||||
image2image: 'Image to Image',
|
||||
upscale: '2 Pass Upscale',
|
||||
flux_schnell: 'Flux Schnell'
|
||||
}
|
||||
},
|
||||
graphCanvasMenu: {
|
||||
zoomIn: 'Увеличить',
|
||||
zoomOut: 'Уменьшить',
|
||||
resetView: 'Сбросить вид',
|
||||
fitView: 'Подгонять под выделенные',
|
||||
selectMode: 'Выбрать режим',
|
||||
panMode: 'Режим панорамирования',
|
||||
toggleLinkVisibility: 'Переключить видимость ссылок'
|
||||
}
|
||||
}
|
||||
// TODO: Add more languages
|
||||
}
|
||||
|
||||
export const i18n = createI18n({
|
||||
// Must set `false`, as Vue I18n Legacy API is for Vue 2
|
||||
legacy: false,
|
||||
locale: navigator.language.split('-')[0] || 'en',
|
||||
fallbackLocale: 'en',
|
||||
messages: {
|
||||
en,
|
||||
zh,
|
||||
ru
|
||||
}
|
||||
messages
|
||||
})
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
export default {
|
||||
welcome: {
|
||||
title: 'Welcome to ComfyUI',
|
||||
getStarted: 'Get Started'
|
||||
},
|
||||
notSupported: {
|
||||
title: 'Your device is not supported',
|
||||
message: 'Only following devices are supported:',
|
||||
learnMore: 'Learn More',
|
||||
reportIssue: 'Report Issue',
|
||||
supportedDevices: {
|
||||
macos: 'MacOS (M1 or later)',
|
||||
windows: 'Windows (Nvidia GPU with CUDA support)'
|
||||
}
|
||||
},
|
||||
install: {
|
||||
installLocation: 'Install Location',
|
||||
migration: 'Migration',
|
||||
desktopSettings: 'Desktop Settings',
|
||||
chooseInstallationLocation: 'Choose Installation Location',
|
||||
systemLocations: 'System Locations',
|
||||
failedToSelectDirectory: 'Failed to select directory',
|
||||
pathValidationFailed: 'Failed to validate path',
|
||||
installLocationDescription:
|
||||
"Select the directory for ComfyUI's user data. A python environment will be installed to the selected location. Please make sure the selected disk has enough space (~15GB) left.",
|
||||
installLocationTooltip:
|
||||
"ComfyUI's user data directory. Stores:\n- Python Environment\n- Models\n- Custom nodes\n",
|
||||
appDataLocationTooltip:
|
||||
"ComfyUI's app data directory. Stores:\n- Logs\n- Server configs",
|
||||
appPathLocationTooltip:
|
||||
"ComfyUI's app asset directory. Stores the ComfyUI code and assets",
|
||||
migrateFromExistingInstallation: 'Migrate from Existing Installation',
|
||||
migrationSourcePathDescription:
|
||||
'If you have an existing ComfyUI installation, we can copy/link your existing user files and models to the new installation.',
|
||||
selectItemsToMigrate: 'Select Items to Migrate',
|
||||
migrationOptional:
|
||||
"Migration is optional. If you don't have an existing installation, you can skip this step.",
|
||||
desktopAppSettings: 'Desktop App Settings',
|
||||
desktopAppSettingsDescription:
|
||||
'Configure how ComfyUI behaves on your desktop. You can change these settings later.',
|
||||
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.',
|
||||
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'
|
||||
}
|
||||
},
|
||||
customNodes: 'Custom Nodes',
|
||||
customNodesDescription:
|
||||
'Reference custom node files from existing ComfyUI installations and install their dependencies.'
|
||||
},
|
||||
serverStart: {
|
||||
reinstall: 'Reinstall',
|
||||
reportIssue: 'Report Issue',
|
||||
openLogs: 'Open Logs',
|
||||
process: {
|
||||
'initial-state': 'Loading...',
|
||||
'python-setup': 'Setting up Python Environment...',
|
||||
'starting-server': 'Starting ComfyUI server...',
|
||||
ready: 'Finishing...',
|
||||
error: 'Unable to start ComfyUI Desktop'
|
||||
}
|
||||
},
|
||||
serverConfig: {
|
||||
modifiedConfigs:
|
||||
'You have modified the following server configurations. Restart to apply changes.',
|
||||
revertChanges: 'Revert Changes',
|
||||
restart: 'Restart'
|
||||
},
|
||||
empty: 'Empty',
|
||||
noWorkflowsFound: 'No workflows found.',
|
||||
comingSoon: 'Coming Soon',
|
||||
firstTimeUIMessage:
|
||||
'This is the first time you use the new UI. Choose "Menu > Use New Menu > Disabled" to restore the old UI.',
|
||||
download: 'Download',
|
||||
loadAllFolders: 'Load All Folders',
|
||||
refresh: 'Refresh',
|
||||
terminal: 'Terminal',
|
||||
logs: 'Logs',
|
||||
videoFailedToLoad: 'Video failed to load',
|
||||
extensionName: 'Extension Name',
|
||||
reloadToApplyChanges: 'Reload to apply changes',
|
||||
insert: 'Insert',
|
||||
systemInfo: 'System Info',
|
||||
devices: 'Devices',
|
||||
about: 'About',
|
||||
add: 'Add',
|
||||
confirm: 'Confirm',
|
||||
reset: 'Reset',
|
||||
resetKeybindingsTooltip: 'Reset keybindings to default',
|
||||
customizeFolder: 'Customize Folder',
|
||||
icon: 'Icon',
|
||||
color: 'Color',
|
||||
bookmark: 'Bookmark',
|
||||
folder: 'Folder',
|
||||
star: 'Star',
|
||||
heart: 'Heart',
|
||||
file: 'File',
|
||||
inbox: 'Inbox',
|
||||
box: 'Box',
|
||||
briefcase: 'Briefcase',
|
||||
error: 'Error',
|
||||
loading: 'Loading',
|
||||
findIssues: 'Find Issues',
|
||||
reportIssue: 'Send Report',
|
||||
reportIssueTooltip: 'Submit the error report to Comfy Org',
|
||||
reportSent: 'Report Submitted',
|
||||
copyToClipboard: 'Copy to Clipboard',
|
||||
openNewIssue: 'Open New Issue',
|
||||
showReport: 'Show Report',
|
||||
imageFailedToLoad: 'Image failed to load',
|
||||
reconnecting: 'Reconnecting',
|
||||
reconnected: 'Reconnected',
|
||||
delete: 'Delete',
|
||||
rename: 'Rename',
|
||||
customize: 'Customize',
|
||||
experimental: 'BETA',
|
||||
deprecated: 'DEPR',
|
||||
loadWorkflow: 'Load Workflow',
|
||||
goToNode: 'Go to Node',
|
||||
settings: 'Settings',
|
||||
searchWorkflows: 'Search Workflows',
|
||||
searchSettings: 'Search Settings',
|
||||
searchNodes: 'Search Nodes',
|
||||
searchModels: 'Search Models',
|
||||
searchKeybindings: 'Search Keybindings',
|
||||
searchExtensions: 'Search Extensions',
|
||||
noResultsFound: 'No Results Found',
|
||||
searchFailedMessage:
|
||||
"We couldn't find any settings matching your search. Try adjusting your search terms.",
|
||||
noTasksFound: 'No Tasks Found',
|
||||
noTasksFoundMessage: 'There are no tasks in the queue.',
|
||||
newFolder: 'New Folder',
|
||||
sideToolbar: {
|
||||
themeToggle: 'Toggle Theme',
|
||||
queue: 'Queue',
|
||||
nodeLibrary: 'Node Library',
|
||||
workflows: 'Workflows',
|
||||
browseTemplates: 'Browse example templates',
|
||||
openWorkflow: 'Open workflow in local file system',
|
||||
newBlankWorkflow: 'Create a new blank workflow',
|
||||
nodeLibraryTab: {
|
||||
sortOrder: 'Sort Order'
|
||||
},
|
||||
modelLibrary: 'Model Library',
|
||||
downloads: 'Downloads',
|
||||
documentation: 'Display documentation for nodes',
|
||||
queueTab: {
|
||||
showFlatList: 'Show Flat List',
|
||||
backToAllTasks: 'Back to All Tasks',
|
||||
containImagePreview: 'Fill Image Preview',
|
||||
coverImagePreview: 'Fit Image Preview',
|
||||
clearPendingTasks: 'Clear Pending Tasks',
|
||||
filter: 'Filter Outputs',
|
||||
filters: {
|
||||
hideCached: 'Hide Cached',
|
||||
hideCanceled: 'Hide Canceled'
|
||||
}
|
||||
}
|
||||
},
|
||||
menu: {
|
||||
hideMenu: 'Hide Menu',
|
||||
showMenu: 'Show Menu',
|
||||
batchCount: 'Batch Count',
|
||||
batchCountTooltip:
|
||||
'The number of times the workflow generation should be queued',
|
||||
autoQueue: 'Auto Queue',
|
||||
disabled: 'Disabled',
|
||||
disabledTooltip: 'The workflow will not be automatically queued',
|
||||
instant: 'Instant',
|
||||
instantTooltip:
|
||||
'The workflow will be queued instantly after a generation finishes',
|
||||
change: 'On Change',
|
||||
changeTooltip: 'The workflow will be queued once a change is made',
|
||||
queueWorkflow: 'Queue workflow (Shift to queue at front)',
|
||||
queueWorkflowFront: 'Queue workflow at front',
|
||||
queue: 'Queue',
|
||||
interrupt: 'Cancel current run',
|
||||
refresh: 'Refresh node definitions',
|
||||
clipspace: 'Open Clipspace',
|
||||
resetView: 'Reset canvas view',
|
||||
clear: 'Clear workflow',
|
||||
toggleBottomPanel: 'Toggle Bottom Panel'
|
||||
},
|
||||
templateWorkflows: {
|
||||
title: 'Get Started with a Template',
|
||||
template: {
|
||||
default: 'Image Generation',
|
||||
image2image: 'Image to Image',
|
||||
upscale: '2 Pass Upscale',
|
||||
flux_schnell: 'Flux Schnell'
|
||||
}
|
||||
},
|
||||
graphCanvasMenu: {
|
||||
zoomIn: 'Zoom In',
|
||||
zoomOut: 'Zoom Out',
|
||||
resetView: 'Reset View',
|
||||
fitView: 'Fit View',
|
||||
selectMode: 'Select Mode',
|
||||
panMode: 'Pan Mode',
|
||||
toggleLinkVisibility: 'Toggle Link Visibility'
|
||||
},
|
||||
electronFileDownload: {
|
||||
inProgress: 'In Progress',
|
||||
pause: 'Pause Download',
|
||||
paused: 'Paused',
|
||||
resume: 'Resume Download',
|
||||
cancel: 'Cancel Download',
|
||||
cancelled: 'Cancelled'
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
export default {
|
||||
empty: 'Пусто',
|
||||
noWorkflowsFound: 'Рабочие процессы не найдены.',
|
||||
download: 'Скачать',
|
||||
refresh: 'Обновить',
|
||||
loadAllFolders: 'Загрузить все папки',
|
||||
terminal: 'Терминал',
|
||||
videoFailedToLoad: 'Видео не удалось загрузить',
|
||||
extensionName: 'Название расширения',
|
||||
reloadToApplyChanges: 'Перезагрузите, чтобы применить изменения',
|
||||
insert: 'Вставить',
|
||||
systemInfo: 'Информация о системе',
|
||||
devices: 'Устройства',
|
||||
about: 'О',
|
||||
add: 'Добавить',
|
||||
confirm: 'Подтвердить',
|
||||
reset: 'Сбросить',
|
||||
resetKeybindingsTooltip: 'Сбросить сочетания клавиш по умолчанию',
|
||||
customizeFolder: 'Настроить папку',
|
||||
icon: 'Иконка',
|
||||
color: 'Цвет',
|
||||
bookmark: 'Закладка',
|
||||
folder: 'Папка',
|
||||
star: 'Звёздочка',
|
||||
heart: 'Сердце',
|
||||
file: 'Файл',
|
||||
inbox: 'Входящие',
|
||||
box: 'Ящик',
|
||||
briefcase: 'Чемодан',
|
||||
error: 'Ошибка',
|
||||
loading: 'Загрузка',
|
||||
findIssues: 'Найти Issue',
|
||||
copyToClipboard: 'Копировать в буфер обмена',
|
||||
openNewIssue: 'Открыть новый Issue',
|
||||
showReport: 'Показать отчёт',
|
||||
imageFailedToLoad: 'Изображение не удалось загрузить',
|
||||
reconnecting: 'Переподключение',
|
||||
reconnected: 'Переподключено',
|
||||
delete: 'Удалить',
|
||||
rename: 'Переименовать',
|
||||
customize: 'Настроить',
|
||||
experimental: 'БЕТА',
|
||||
deprecated: 'УСТАР',
|
||||
loadWorkflow: 'Загрузить рабочий процесс',
|
||||
goToNode: 'Перейти к узлу',
|
||||
settings: 'Настройки',
|
||||
searchWorkflows: 'Поиск рабочих процессов',
|
||||
searchSettings: 'Поиск настроек',
|
||||
searchNodes: 'Поиск узлов',
|
||||
searchModels: 'Поиск моделей',
|
||||
searchKeybindings: 'Поиск сочетаний клавиш',
|
||||
searchExtensions: 'Поиск расширений',
|
||||
noResultsFound: 'Ничего не найдено',
|
||||
searchFailedMessage:
|
||||
'Не удалось найти ни одной настройки, соответствующей вашему запросу. Попробуйте скорректировать поисковый запрос.',
|
||||
noContent: '(Нет контента)',
|
||||
noTasksFound: 'Задачи не найдены',
|
||||
noTasksFoundMessage: 'В очереди нет задач.',
|
||||
newFolder: 'Новая папка',
|
||||
sideToolbar: {
|
||||
themeToggle: 'Переключить тему',
|
||||
queue: 'Очередь',
|
||||
nodeLibrary: 'Библиотека узлов',
|
||||
workflows: 'Рабочие процессы',
|
||||
browseTemplates: 'Просмотреть примеры шаблонов',
|
||||
openWorkflow: 'Открыть рабочий процесс в локальной файловой системе',
|
||||
newBlankWorkflow: 'Создайте новый пустой рабочий процесс',
|
||||
nodeLibraryTab: {
|
||||
sortOrder: 'Порядок сортировки'
|
||||
},
|
||||
modelLibrary: 'Библиотека моделей',
|
||||
queueTab: {
|
||||
showFlatList: 'Показать плоский список',
|
||||
backToAllTasks: 'Вернуться ко всем задачам',
|
||||
containImagePreview: 'Предпросмотр заливающего изображения',
|
||||
coverImagePreview: 'Предпросмотр подходящего изображения',
|
||||
clearPendingTasks: 'Очистить отложенные задачи'
|
||||
}
|
||||
},
|
||||
menu: {
|
||||
hideMenu: 'Скрыть меню',
|
||||
showMenu: 'Показать меню',
|
||||
batchCount: 'Количество пакетов',
|
||||
batchCountTooltip:
|
||||
'Количество раз, когда генерация рабочего процесса должна быть помещена в очередь',
|
||||
autoQueue: 'Автоочередь',
|
||||
disabled: 'Отключено',
|
||||
disabledTooltip: 'Рабочий процесс не будет автоматически помещён в очередь',
|
||||
instant: 'Мгновенно',
|
||||
instantTooltip:
|
||||
'Рабочий процесс будет помещён в очередь сразу же после завершения генерации',
|
||||
change: 'При изменении',
|
||||
changeTooltip:
|
||||
'Рабочий процесс будет поставлен в очередь после внесения изменений',
|
||||
queueWorkflow: 'Очередь рабочего процесса (Shift для вставки спереди)',
|
||||
queueWorkflowFront: 'Очередь рабочего процесса (Вставка спереди)',
|
||||
queue: 'Очередь',
|
||||
interrupt: 'Отменить текущее выполнение',
|
||||
refresh: 'Обновить определения узлов',
|
||||
clipspace: 'Открыть Clipspace',
|
||||
resetView: 'Сбросить вид холста',
|
||||
clear: 'Очистить рабочий процесс'
|
||||
},
|
||||
templateWorkflows: {
|
||||
title: 'Начните работу с шаблона',
|
||||
template: {
|
||||
default: 'Image Generation',
|
||||
image2image: 'Image to Image',
|
||||
upscale: '2 Pass Upscale',
|
||||
flux_schnell: 'Flux Schnell'
|
||||
}
|
||||
},
|
||||
graphCanvasMenu: {
|
||||
zoomIn: 'Увеличить',
|
||||
zoomOut: 'Уменьшить',
|
||||
resetView: 'Сбросить вид',
|
||||
fitView: 'Подгонять под выделенные',
|
||||
selectMode: 'Выбрать режим',
|
||||
panMode: 'Режим панорамирования',
|
||||
toggleLinkVisibility: 'Переключить видимость ссылок'
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
export default {
|
||||
empty: '空',
|
||||
noWorkflowsFound: '未找到工作流',
|
||||
firstTimeUIMessage:
|
||||
'这是您第一次使用新界面。选择“Menu > Use New Menu > Disabled”以恢复旧界面。',
|
||||
download: '下载',
|
||||
loadAllFolders: '加载所有文件夹',
|
||||
refresh: '刷新',
|
||||
terminal: '终端',
|
||||
videoFailedToLoad: '视频加载失败',
|
||||
extensionName: '扩展名称',
|
||||
reloadToApplyChanges: '重新加载以应用更改',
|
||||
insert: '插入',
|
||||
systemInfo: '系统信息',
|
||||
devices: '设备',
|
||||
about: '关于',
|
||||
add: '添加',
|
||||
confirm: '确认',
|
||||
reset: '重置',
|
||||
resetKeybindingsTooltip: '重置键位',
|
||||
customizeFolder: '定制文件夹',
|
||||
icon: '图标',
|
||||
color: '颜色',
|
||||
bookmark: '书签',
|
||||
folder: '文件夹',
|
||||
star: '星星',
|
||||
heart: '心',
|
||||
file: '文件',
|
||||
inbox: '收件箱',
|
||||
box: '盒子',
|
||||
briefcase: '公文包',
|
||||
error: '错误',
|
||||
loading: '加载中',
|
||||
findIssues: '查找 Issue',
|
||||
copyToClipboard: '复制到剪贴板',
|
||||
openNewIssue: '开启新 Issue',
|
||||
showReport: '显示报告',
|
||||
imageFailedToLoad: '图像加载失败',
|
||||
reconnecting: '重新连接中',
|
||||
reconnected: '已重新连接',
|
||||
delete: '删除',
|
||||
rename: '重命名',
|
||||
customize: '定制',
|
||||
experimental: 'BETA',
|
||||
deprecated: '弃用',
|
||||
loadWorkflow: '加载工作流',
|
||||
goToNode: '前往节点',
|
||||
settings: '设置',
|
||||
searchWorkflows: '搜索工作流',
|
||||
searchSettings: '搜索设置',
|
||||
searchNodes: '搜索节点',
|
||||
searchModels: '搜索模型',
|
||||
searchKeybindings: '搜索键位',
|
||||
searchExtensions: '搜索插件',
|
||||
noResultsFound: '未找到结果',
|
||||
searchFailedMessage:
|
||||
'我们找不到与您的搜索匹配的任何设置。请尝试调整搜索条件。',
|
||||
noContent: '(无内容)',
|
||||
noTasksFound: '未找到任务',
|
||||
noTasksFoundMessage: '队列中没有任务。',
|
||||
newFolder: '新建文件夹',
|
||||
sideToolbar: {
|
||||
themeToggle: '主题切换',
|
||||
queue: '队列',
|
||||
nodeLibrary: '节点库',
|
||||
workflows: '工作流',
|
||||
browseTemplates: '浏览示例模板',
|
||||
openWorkflow: '在本地文件系统中打开工作流',
|
||||
newBlankWorkflow: '创建一个新空白工作流',
|
||||
nodeLibraryTab: {
|
||||
sortOrder: '排序顺序'
|
||||
},
|
||||
modelLibrary: '模型库',
|
||||
queueTab: {
|
||||
showFlatList: '平铺结果',
|
||||
backToAllTasks: '返回',
|
||||
containImagePreview: '填充图像预览',
|
||||
coverImagePreview: '适应图像预览',
|
||||
clearPendingTasks: '清除待处理任务'
|
||||
}
|
||||
},
|
||||
menu: {
|
||||
hideMenu: '隐藏菜单',
|
||||
showMenu: '显示菜单',
|
||||
batchCount: '批次数量',
|
||||
batchCountTooltip: '工作流生成次数',
|
||||
autoQueue: '自动执行',
|
||||
disabled: '禁用',
|
||||
disabledTooltip: '工作流将不会自动执行',
|
||||
instant: '实时',
|
||||
instantTooltip: '工作流将会在生成完成后立即执行',
|
||||
change: '变动',
|
||||
changeTooltip: '工作流将会在改变后执行',
|
||||
queueWorkflow: '执行 (Shift 执行到队列首)',
|
||||
queueWorkflowFront: '执行到队列首',
|
||||
queue: '队列',
|
||||
interrupt: '取消当前任务',
|
||||
refresh: '刷新节点',
|
||||
clipspace: '打开剪贴板',
|
||||
resetView: '重置画布视图',
|
||||
clear: '清空工作流',
|
||||
toggleBottomPanel: '底部面板'
|
||||
},
|
||||
templateWorkflows: {
|
||||
title: '从模板开始',
|
||||
template: {
|
||||
default: 'Image Generation',
|
||||
image2image: 'Image to Image',
|
||||
upscale: '2 Pass Upscale',
|
||||
flux_schnell: 'Flux Schnell'
|
||||
}
|
||||
},
|
||||
graphCanvasMenu: {
|
||||
zoomIn: '放大',
|
||||
zoomOut: '缩小',
|
||||
resetView: '重置视图',
|
||||
fitView: '适应视图',
|
||||
selectMode: '选择模式',
|
||||
panMode: '平移模式',
|
||||
toggleLinkVisibility: '切换链接可见性'
|
||||
}
|
||||
}
|
||||
@@ -57,12 +57,6 @@ const router = createRouter({
|
||||
name: 'WelcomeView',
|
||||
component: () => import('@/views/WelcomeView.vue'),
|
||||
beforeEnter: guardElectronAccess
|
||||
},
|
||||
{
|
||||
path: 'not-supported',
|
||||
name: 'NotSupportedView',
|
||||
component: () => import('@/views/NotSupportedView.vue'),
|
||||
beforeEnter: guardElectronAccess
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -137,7 +137,6 @@ export class ComfyApp {
|
||||
canvas: LGraphCanvas
|
||||
dragOverNode: LGraphNode | null
|
||||
canvasEl: HTMLCanvasElement
|
||||
tooltipCallback?: (node: LGraphNode, name: string, value?: string) => boolean
|
||||
// x, y, scale
|
||||
zoom_drag_start: [number, number, number] | null
|
||||
lastNodeErrors: any[] | null
|
||||
@@ -146,6 +145,7 @@ export class ComfyApp {
|
||||
configuringGraph: boolean
|
||||
isNewUserSession: boolean
|
||||
storageLocation: StorageLocation
|
||||
multiUserServer: boolean
|
||||
ctx: CanvasRenderingContext2D
|
||||
bodyTop: HTMLElement
|
||||
bodyLeft: HTMLElement
|
||||
@@ -1728,6 +1728,7 @@ export class ComfyApp {
|
||||
return
|
||||
}
|
||||
|
||||
this.multiUserServer = true
|
||||
let user = localStorage['Comfy.userId']
|
||||
const users = userConfig.users ?? {}
|
||||
if (!user || !users[user]) {
|
||||
|
||||
@@ -388,11 +388,8 @@ export class ChangeTracker {
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare extra properties ignoring ds
|
||||
if (
|
||||
!_.isEqual(_.omit(a.extra ?? {}, ['ds']), _.omit(b.extra ?? {}, ['ds']))
|
||||
)
|
||||
return false
|
||||
// Reroutes (schema 0.4)
|
||||
if (!_.isEqual(a.extra?.reroutes, b.extra?.reroutes)) return false
|
||||
|
||||
// Compare other properties normally
|
||||
for (const key of ['links', 'reroutes', 'groups']) {
|
||||
|
||||
@@ -66,7 +66,7 @@ export const workflowService = {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const existingWorkflow = workflowStore.getWorkflowByPath(newPath)
|
||||
|
||||
if (existingWorkflow && !existingWorkflow.isTemporary) {
|
||||
if (existingWorkflow) {
|
||||
const res = (await ComfyAsyncDialog.prompt({
|
||||
title: 'Overwrite existing file?',
|
||||
message: `"${newPath}" already exists. Do you want to overwrite it?`,
|
||||
|
||||
@@ -2,126 +2,52 @@
|
||||
// Currently we need to bridge between legacy app code and Vue app with a Pinia store.
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, type Component, markRaw } from 'vue'
|
||||
import { ref, shallowRef, type Component, markRaw } from 'vue'
|
||||
|
||||
interface DialogComponentProps {
|
||||
maximizable?: boolean
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
interface DialogInstance {
|
||||
key: string
|
||||
visible: boolean
|
||||
title?: string
|
||||
headerComponent?: Component
|
||||
component: Component
|
||||
contentProps: Record<string, any>
|
||||
dialogComponentProps: Record<string, any>
|
||||
}
|
||||
|
||||
export const useDialogStore = defineStore('dialog', () => {
|
||||
const dialogStack = ref<DialogInstance[]>([])
|
||||
|
||||
const genDialogKey = () => `dialog-${Math.random().toString(36).slice(2, 9)}`
|
||||
|
||||
function riseDialog(options: { key: string }) {
|
||||
const dialogKey = options.key
|
||||
|
||||
const index = dialogStack.value.findIndex((d) => d.key === dialogKey)
|
||||
if (index !== -1) {
|
||||
const dialogs = dialogStack.value.splice(index, 1)
|
||||
dialogStack.value.push(...dialogs)
|
||||
}
|
||||
}
|
||||
|
||||
function closeDialog(options?: { key: string }) {
|
||||
if (!options) {
|
||||
dialogStack.value.pop()
|
||||
return
|
||||
}
|
||||
|
||||
const dialogKey = options.key
|
||||
|
||||
const index = dialogStack.value.findIndex((d) => d.key === dialogKey)
|
||||
if (index === -1) {
|
||||
return
|
||||
}
|
||||
dialogStack.value.splice(index, 1)
|
||||
}
|
||||
|
||||
function createDialog(options: {
|
||||
key: string
|
||||
title?: string
|
||||
headerComponent?: Component
|
||||
component: Component
|
||||
props?: Record<string, any>
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
}) {
|
||||
const dialog = {
|
||||
key: options.key,
|
||||
visible: true,
|
||||
title: options.title,
|
||||
headerComponent: options.headerComponent
|
||||
? markRaw(options.headerComponent)
|
||||
: undefined,
|
||||
component: markRaw(options.component),
|
||||
contentProps: { ...options.props },
|
||||
dialogComponentProps: {
|
||||
maximizable: false,
|
||||
modal: true,
|
||||
closable: true,
|
||||
closeOnEscape: true,
|
||||
dismissableMask: true,
|
||||
...options.dialogComponentProps,
|
||||
maximized: false,
|
||||
onMaximize: () => {
|
||||
dialog.dialogComponentProps.maximized = true
|
||||
},
|
||||
onUnmaximize: () => {
|
||||
dialog.dialogComponentProps.maximized = false
|
||||
},
|
||||
onAfterHide: () => {
|
||||
options.dialogComponentProps?.onClose?.()
|
||||
closeDialog(dialog)
|
||||
},
|
||||
pt: {
|
||||
root: {
|
||||
onMousedown: () => {
|
||||
riseDialog(dialog)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dialogStack.value.push(dialog)
|
||||
|
||||
return dialog
|
||||
}
|
||||
const isVisible = ref(false)
|
||||
const title = ref('')
|
||||
const headerComponent = shallowRef<Component | null>(null)
|
||||
const component = shallowRef<Component | null>(null)
|
||||
const props = ref<Record<string, any>>({})
|
||||
const dialogComponentProps = ref<DialogComponentProps>({})
|
||||
|
||||
function showDialog(options: {
|
||||
key?: string
|
||||
title?: string
|
||||
headerComponent?: Component
|
||||
component: Component
|
||||
props?: Record<string, any>
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
}) {
|
||||
const dialogKey = options.key || genDialogKey()
|
||||
isVisible.value = true
|
||||
title.value = options.title ?? ''
|
||||
headerComponent.value = options.headerComponent
|
||||
? markRaw(options.headerComponent)
|
||||
: null
|
||||
component.value = markRaw(options.component)
|
||||
props.value = options.props || {}
|
||||
dialogComponentProps.value = options.dialogComponentProps || {}
|
||||
}
|
||||
|
||||
let dialog = dialogStack.value.find((d) => d.key === dialogKey)
|
||||
|
||||
if (dialog) {
|
||||
dialog.visible = true
|
||||
riseDialog(dialog)
|
||||
} else {
|
||||
dialog = createDialog({ ...options, key: dialogKey })
|
||||
function closeDialog() {
|
||||
if (dialogComponentProps.value.onClose) {
|
||||
dialogComponentProps.value.onClose()
|
||||
}
|
||||
return dialog
|
||||
isVisible.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
dialogStack,
|
||||
riseDialog,
|
||||
isVisible,
|
||||
title,
|
||||
headerComponent,
|
||||
component,
|
||||
props,
|
||||
dialogComponentProps,
|
||||
showDialog,
|
||||
closeDialog
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { isElectron, electronAPI } from '@/utils/envUtil'
|
||||
import {
|
||||
type DownloadState,
|
||||
import type {
|
||||
DownloadState,
|
||||
DownloadStatus
|
||||
} from '@comfyorg/comfyui-electron-types'
|
||||
|
||||
@@ -69,11 +69,6 @@ export const useElectronDownloadStore = defineStore('downloads', () => {
|
||||
resume,
|
||||
cancel,
|
||||
findByUrl,
|
||||
initialize,
|
||||
inProgressDownloads: computed(() =>
|
||||
downloads.value.filter(
|
||||
({ status }) => status !== DownloadStatus.COMPLETED
|
||||
)
|
||||
)
|
||||
initialize
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { LGraphNode, LGraphGroup, LGraphCanvas } from '@comfyorg/litegraph'
|
||||
import type { ComfyNodeItem } from '@/types/comfy'
|
||||
import { defineStore } from 'pinia'
|
||||
import { shallowRef } from 'vue'
|
||||
|
||||
@@ -11,14 +10,6 @@ export const useTitleEditorStore = defineStore('titleEditor', () => {
|
||||
}
|
||||
})
|
||||
|
||||
export const useHoveredItemStore = defineStore('hoveredItem', () => {
|
||||
const hoveredItem = shallowRef<ComfyNodeItem | null>(null)
|
||||
|
||||
return {
|
||||
hoveredItem
|
||||
}
|
||||
})
|
||||
|
||||
export const useCanvasStore = defineStore('canvas', () => {
|
||||
/**
|
||||
* The LGraphCanvas instance.
|
||||
|
||||
@@ -5,8 +5,7 @@ import {
|
||||
import {
|
||||
type ComfyNodeDef,
|
||||
type ComfyInputsSpec as ComfyInputsSpecSchema,
|
||||
type InputSpec,
|
||||
type DescriptionSpec
|
||||
type InputSpec
|
||||
} from '@/types/apiTypes'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ComfyWidgetConstructor } from '@/scripts/widgets'
|
||||
@@ -159,7 +158,7 @@ export class ComfyNodeDefImpl {
|
||||
display_name: string
|
||||
category: string
|
||||
python_module: string
|
||||
description: DescriptionSpec
|
||||
description: string
|
||||
deprecated: boolean
|
||||
experimental: boolean
|
||||
input: ComfyInputsSpec
|
||||
|
||||
@@ -30,6 +30,7 @@ export class ResultItemImpl {
|
||||
filename: string
|
||||
subfolder: string
|
||||
type: string
|
||||
cached: boolean
|
||||
|
||||
nodeId: NodeId
|
||||
// 'audio' | 'images' | ...
|
||||
@@ -43,6 +44,7 @@ export class ResultItemImpl {
|
||||
this.filename = obj.filename ?? ''
|
||||
this.subfolder = obj.subfolder ?? ''
|
||||
this.type = obj.type ?? ''
|
||||
this.cached = obj.cached
|
||||
|
||||
this.nodeId = obj.nodeId
|
||||
this.mediaType = obj.mediaType
|
||||
@@ -149,6 +151,13 @@ export class TaskItemImpl {
|
||||
if (!this.outputs) {
|
||||
return []
|
||||
}
|
||||
|
||||
const cachedEntries = new Set(
|
||||
this.status?.messages?.find((message) => {
|
||||
return message[0] === 'execution_cached'
|
||||
})?.[1].nodes
|
||||
)
|
||||
|
||||
return Object.entries(this.outputs).flatMap(([nodeId, nodeOutputs]) =>
|
||||
Object.entries(nodeOutputs).flatMap(([mediaType, items]) =>
|
||||
(items as ResultItem[]).map(
|
||||
@@ -156,7 +165,8 @@ export class TaskItemImpl {
|
||||
new ResultItemImpl({
|
||||
...item,
|
||||
nodeId,
|
||||
mediaType
|
||||
mediaType,
|
||||
cached: cachedEntries.has(nodeId)
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,39 +1,18 @@
|
||||
import { ServerConfig, ServerConfigValue } from '@/constants/serverConfig'
|
||||
import { ServerConfig } from '@/constants/serverConfig'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
export type ServerConfigWithValue<T> = ServerConfig<T> & {
|
||||
/**
|
||||
* Current value.
|
||||
*/
|
||||
value: T
|
||||
/**
|
||||
* Initial value loaded from settings.
|
||||
*/
|
||||
initialValue: T
|
||||
}
|
||||
|
||||
export const useServerConfigStore = defineStore('serverConfig', () => {
|
||||
const serverConfigById = ref<
|
||||
Record<string, ServerConfigWithValue<ServerConfigValue>>
|
||||
>({})
|
||||
const serverConfigById = ref<Record<string, ServerConfigWithValue<any>>>({})
|
||||
const serverConfigs = computed(() => {
|
||||
return Object.values(serverConfigById.value)
|
||||
})
|
||||
const modifiedConfigs = computed<ServerConfigWithValue<ServerConfigValue>[]>(
|
||||
() => {
|
||||
return serverConfigs.value.filter((config) => {
|
||||
return config.initialValue !== config.value
|
||||
})
|
||||
}
|
||||
)
|
||||
const revertChanges = () => {
|
||||
for (const config of modifiedConfigs.value) {
|
||||
config.value = config.initialValue
|
||||
}
|
||||
}
|
||||
const serverConfigsByCategory = computed<
|
||||
Record<string, ServerConfigWithValue<ServerConfigValue>[]>
|
||||
Record<string, ServerConfigWithValue<any>[]>
|
||||
>(() => {
|
||||
return serverConfigs.value.reduce(
|
||||
(acc, config) => {
|
||||
@@ -42,17 +21,15 @@ export const useServerConfigStore = defineStore('serverConfig', () => {
|
||||
acc[category].push(config)
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, ServerConfigWithValue<ServerConfigValue>[]>
|
||||
{} as Record<string, ServerConfigWithValue<any>[]>
|
||||
)
|
||||
})
|
||||
const serverConfigValues = computed<Record<string, ServerConfigValue>>(() => {
|
||||
const serverConfigValues = computed<Record<string, any>>(() => {
|
||||
return Object.fromEntries(
|
||||
serverConfigs.value.map((config) => {
|
||||
return [
|
||||
config.id,
|
||||
config.value === config.defaultValue ||
|
||||
config.value === null ||
|
||||
config.value === undefined
|
||||
config.value === config.defaultValue || !config.value
|
||||
? undefined
|
||||
: config.value
|
||||
]
|
||||
@@ -60,18 +37,10 @@ export const useServerConfigStore = defineStore('serverConfig', () => {
|
||||
)
|
||||
})
|
||||
const launchArgs = computed<Record<string, string>>(() => {
|
||||
const args: Record<
|
||||
string,
|
||||
Omit<ServerConfigValue, 'undefined' | 'null'>
|
||||
> = Object.assign(
|
||||
return Object.assign(
|
||||
{},
|
||||
...serverConfigs.value.map((config) => {
|
||||
// Filter out configs that have the default value or undefined | null value
|
||||
if (
|
||||
config.value === config.defaultValue ||
|
||||
config.value === null ||
|
||||
config.value === undefined
|
||||
) {
|
||||
if (config.value === config.defaultValue || !config.value) {
|
||||
return {}
|
||||
}
|
||||
return config.getValue
|
||||
@@ -79,36 +48,16 @@ export const useServerConfigStore = defineStore('serverConfig', () => {
|
||||
: { [config.id]: config.value }
|
||||
})
|
||||
)
|
||||
|
||||
// Convert true to empty string
|
||||
// Convert number to string
|
||||
return Object.fromEntries(
|
||||
Object.entries(args).map(([key, value]) => {
|
||||
if (value === true) {
|
||||
return [key, '']
|
||||
}
|
||||
return [key, value.toString()]
|
||||
})
|
||||
) as Record<string, string>
|
||||
})
|
||||
const commandLineArgs = computed<string>(() => {
|
||||
return Object.entries(launchArgs.value)
|
||||
.map(([key, value]) => [`--${key}`, value])
|
||||
.flat()
|
||||
.filter((arg: string) => arg !== '')
|
||||
.join(' ')
|
||||
})
|
||||
|
||||
function loadServerConfig(
|
||||
configs: ServerConfig<ServerConfigValue>[],
|
||||
values: Record<string, ServerConfigValue>
|
||||
configs: ServerConfig<any>[],
|
||||
values: Record<string, any>
|
||||
) {
|
||||
for (const config of configs) {
|
||||
const value = values[config.id] ?? config.defaultValue
|
||||
serverConfigById.value[config.id] = {
|
||||
...config,
|
||||
value,
|
||||
initialValue: value
|
||||
value: values[config.id] ?? config.defaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,12 +65,9 @@ export const useServerConfigStore = defineStore('serverConfig', () => {
|
||||
return {
|
||||
serverConfigById,
|
||||
serverConfigs,
|
||||
modifiedConfigs,
|
||||
serverConfigsByCategory,
|
||||
serverConfigValues,
|
||||
launchArgs,
|
||||
commandLineArgs,
|
||||
revertChanges,
|
||||
loadServerConfig
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useModelLibrarySidebarTab } from '@/hooks/sidebarTabs/modelLibrarySideb
|
||||
import { useNodeLibrarySidebarTab } from '@/hooks/sidebarTabs/nodeLibrarySidebarTab'
|
||||
import { useQueueSidebarTab } from '@/hooks/sidebarTabs/queueSidebarTab'
|
||||
import { useWorkflowsSidebarTab } from '@/hooks/sidebarTabs/workflowsSidebarTab'
|
||||
import { useDocumentationSidebarTab } from '@/hooks/sidebarTabs/documentationSidebarTab'
|
||||
import { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
@@ -58,7 +57,6 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
|
||||
registerSidebarTab(useNodeLibrarySidebarTab())
|
||||
registerSidebarTab(useModelLibrarySidebarTab())
|
||||
registerSidebarTab(useWorkflowsSidebarTab())
|
||||
registerSidebarTab(useDocumentationSidebarTab())
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -356,11 +356,6 @@ const zComfyComboOutput = z.array(z.any())
|
||||
const zComfyOutputTypesSpec = z.array(
|
||||
z.union([zComfyNodeDataType, zComfyComboOutput])
|
||||
)
|
||||
const zDescriptionSpec = z.union([
|
||||
z.string(),
|
||||
z.tuple([z.string(), z.string()]),
|
||||
z.tuple([z.string(), z.string(), z.record(z.string(), z.any())])
|
||||
])
|
||||
|
||||
const zComfyNodeDef = z.object({
|
||||
input: zComfyInputsSpec.optional(),
|
||||
@@ -370,7 +365,7 @@ const zComfyNodeDef = z.object({
|
||||
output_tooltips: z.array(z.string()).optional(),
|
||||
name: z.string(),
|
||||
display_name: z.string(),
|
||||
description: zDescriptionSpec,
|
||||
description: z.string(),
|
||||
category: z.string(),
|
||||
output_node: z.boolean(),
|
||||
python_module: z.string(),
|
||||
@@ -383,7 +378,6 @@ export type InputSpec = z.infer<typeof zInputSpec>
|
||||
export type ComfyInputsSpec = z.infer<typeof zComfyInputsSpec>
|
||||
export type ComfyOutputTypesSpec = z.infer<typeof zComfyOutputTypesSpec>
|
||||
export type ComfyNodeDef = z.infer<typeof zComfyNodeDef>
|
||||
export type DescriptionSpec = z.infer<typeof zDescriptionSpec>
|
||||
|
||||
export function validateComfyNodeDef(
|
||||
data: any,
|
||||
@@ -460,6 +454,11 @@ const zNodeBadgeMode = z.enum(
|
||||
Object.values(NodeBadgeMode) as [string, ...string[]]
|
||||
)
|
||||
|
||||
const zQueueFilter = z.object({
|
||||
hideCached: z.boolean(),
|
||||
hideCanceled: z.boolean()
|
||||
})
|
||||
|
||||
const zSettings = z.record(z.any()).and(
|
||||
z
|
||||
.object({
|
||||
@@ -512,6 +511,8 @@ const zSettings = z.record(z.any()).and(
|
||||
'Comfy.Validation.Workflows': z.boolean(),
|
||||
'Comfy.Workflow.SortNodeIdOnSave': z.boolean(),
|
||||
'Comfy.Queue.ImageFit': z.enum(['contain', 'cover']),
|
||||
'Comfy.Queue.ShowFlatList': z.boolean(),
|
||||
'Comfy.Queue.Filter': zQueueFilter.passthrough(),
|
||||
'Comfy.Workflow.WorkflowTabsPosition': z.enum(['Sidebar', 'Topbar']),
|
||||
'Comfy.Node.DoubleClickTitleToEdit': z.boolean(),
|
||||
'Comfy.Window.UnloadConfirmation': z.boolean(),
|
||||
|
||||
33
src/types/comfy.ts → src/types/comfy.d.ts
vendored
33
src/types/comfy.ts → src/types/comfy.d.ts
vendored
@@ -1,12 +1,11 @@
|
||||
import type { LGraphNode, IWidget } from '@comfyorg/litegraph'
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
import type { ComfyNodeDef, DescriptionSpec } from '@/types/apiTypes'
|
||||
import type { LGraphNode } from './litegraph'
|
||||
import type { ComfyApp } from '../scripts/app'
|
||||
import type { ComfyNodeDef } from '@/types/apiTypes'
|
||||
import type { Keybinding } from '@/types/keyBindingTypes'
|
||||
import type { ComfyCommand } from '@/stores/commandStore'
|
||||
import type { SettingParams } from '@/types/settingTypes'
|
||||
import type { BottomPanelExtension } from '@/types/extensionTypes'
|
||||
import type { SettingParams } from './settingTypes'
|
||||
import type { BottomPanelExtension } from './extensionTypes'
|
||||
import type { ComfyWidgetConstructor } from '@/scripts/widgets'
|
||||
import type { ComfyWorkflowJSON } from '@/types/comfyWorkflow'
|
||||
|
||||
export type Widgets = Record<string, ComfyWidgetConstructor>
|
||||
|
||||
@@ -160,8 +159,20 @@ export interface ComfyExtension {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export type ComfyNodeItem =
|
||||
| { node: LGraphNode; type: 'Title' }
|
||||
| { node: LGraphNode; type: 'Output'; outputSlot: number }
|
||||
| { node: LGraphNode; type: 'Input'; inputName: string }
|
||||
| { node: LGraphNode; type: 'Widget'; widget: IWidget }
|
||||
/**
|
||||
* @deprecated Use ComfyNodeDef instead
|
||||
*/
|
||||
export type ComfyObjectInfo = {
|
||||
name: string
|
||||
display_name?: string
|
||||
description?: string
|
||||
category: string
|
||||
input?: {
|
||||
required?: Record<string, ComfyObjectInfoConfig>
|
||||
optional?: Record<string, ComfyObjectInfoConfig>
|
||||
}
|
||||
output?: string[]
|
||||
output_name: string[]
|
||||
}
|
||||
|
||||
export type ComfyObjectInfoConfig = [string | any[]] | [string | any[], any]
|
||||
@@ -40,7 +40,6 @@ export enum CudaMalloc {
|
||||
|
||||
export enum FloatingPointPrecision {
|
||||
AUTO = 'auto',
|
||||
FP64 = 'fp64',
|
||||
FP32 = 'fp32',
|
||||
FP16 = 'fp16',
|
||||
BF16 = 'bf16',
|
||||
|
||||
@@ -58,8 +58,3 @@ export interface FormItem {
|
||||
attrs?: Record<string, any>
|
||||
options?: Array<string | SettingOption> | ((value: any) => SettingOption[])
|
||||
}
|
||||
|
||||
export interface ISettingGroup {
|
||||
label: string
|
||||
settings: SettingParams[]
|
||||
}
|
||||
|
||||
@@ -19,10 +19,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { StatusWsMessageStatus } from '@/types/apiTypes'
|
||||
import {
|
||||
useQueuePendingTaskCountStore,
|
||||
useQueueStore
|
||||
} from '@/stores/queueStore'
|
||||
import { useQueuePendingTaskCountStore } from '@/stores/queueStore'
|
||||
import type { ToastMessageOptions } from 'primevue/toast'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { i18n } from '@/i18n'
|
||||
@@ -38,8 +35,6 @@ import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
import { useNodeDefStore, useNodeFrequencyStore } from '@/stores/nodeDefStore'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
import { useModelStore } from '@/stores/modelStore'
|
||||
import { useServerConfigStore } from '@/stores/serverConfigStore'
|
||||
import { SERVER_CONFIG_ITEMS } from '@/constants/serverConfig'
|
||||
|
||||
setupAutoQueueHandler()
|
||||
|
||||
@@ -97,12 +92,6 @@ watchEffect(() => {
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
useQueueStore().maxHistoryItems = settingStore.get(
|
||||
'Comfy.Queue.MaxHistoryItems'
|
||||
)
|
||||
})
|
||||
|
||||
const init = () => {
|
||||
settingStore.addSettings(app.ui.settings)
|
||||
useKeybindingStore().loadCoreKeybindings()
|
||||
@@ -162,12 +151,6 @@ const onGraphReady = () => {
|
||||
// Load keybindings.
|
||||
useKeybindingStore().loadUserKeybindings()
|
||||
|
||||
// Load server config
|
||||
useServerConfigStore().loadServerConfig(
|
||||
SERVER_CONFIG_ITEMS,
|
||||
settingStore.get('Comfy.Server.ServerConfigValues')
|
||||
)
|
||||
|
||||
// Load model folders
|
||||
useModelStore().loadModelFolders()
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="font-sans w-screen h-screen flex items-center m-0 text-neutral-900 bg-neutral-300 pointer-events-auto"
|
||||
>
|
||||
<div class="flex-grow flex items-center justify-center">
|
||||
<div class="flex flex-col gap-8 p-8">
|
||||
<!-- Header -->
|
||||
<h1 class="text-4xl font-bold text-red-500">
|
||||
{{ $t('notSupported.title') }}
|
||||
</h1>
|
||||
|
||||
<!-- Message -->
|
||||
<div class="space-y-4">
|
||||
<p class="text-xl">
|
||||
{{ $t('notSupported.message') }}
|
||||
</p>
|
||||
<ul class="list-disc list-inside space-y-1 text-neutral-800">
|
||||
<li>{{ $t('notSupported.supportedDevices.macos') }}</li>
|
||||
<li>{{ $t('notSupported.supportedDevices.windows') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-4">
|
||||
<Button
|
||||
:label="$t('notSupported.learnMore')"
|
||||
icon="pi pi-github"
|
||||
@click="openDocs"
|
||||
severity="secondary"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('notSupported.reportIssue')"
|
||||
icon="pi pi-flag"
|
||||
@click="reportIssue"
|
||||
severity="danger"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right side image -->
|
||||
<div class="h-screen flex-grow-0">
|
||||
<img
|
||||
src="/assets/images/sad_girl.png"
|
||||
alt="Sad girl illustration"
|
||||
class="h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
const openDocs = () => {
|
||||
window.open('https://github.com/Comfy-Org/desktop', '_blank')
|
||||
}
|
||||
|
||||
const reportIssue = () => {
|
||||
window.open('https://forum.comfy.org/c/v1-feedback/', '_blank')
|
||||
}
|
||||
</script>
|
||||
@@ -2,14 +2,9 @@
|
||||
<div
|
||||
class="font-sans flex flex-col justify-center items-center h-screen m-0 text-neutral-300 bg-neutral-900 dark-theme pointer-events-auto"
|
||||
>
|
||||
<h2 class="text-2xl font-bold">
|
||||
{{ t(`serverStart.process.${status}`) }}
|
||||
<span v-if="status === ProgressStatus.ERROR">
|
||||
v{{ electronVersion }}
|
||||
</span>
|
||||
</h2>
|
||||
<h2 class="text-2xl font-bold">{{ t(`serverStart.process.${status}`) }}</h2>
|
||||
<div
|
||||
v-if="status === ProgressStatus.ERROR"
|
||||
v-if="status == ProgressStatus.ERROR"
|
||||
class="flex items-center my-4 gap-2"
|
||||
>
|
||||
<Button
|
||||
@@ -48,7 +43,6 @@ const electron = electronAPI()
|
||||
const { t } = useI18n()
|
||||
|
||||
const status = ref<ProgressStatus>(ProgressStatus.INITIAL_STATE)
|
||||
const electronVersion = ref<string>('')
|
||||
let xterm: Terminal | undefined
|
||||
|
||||
const updateProgress = ({ status: newStatus }: { status: ProgressStatus }) => {
|
||||
@@ -78,10 +72,9 @@ const reportIssue = () => {
|
||||
}
|
||||
const openLogs = () => electron.openLogsFolder()
|
||||
|
||||
onMounted(async () => {
|
||||
onMounted(() => {
|
||||
electron.sendReady()
|
||||
electron.onProgressUpdate(updateProgress)
|
||||
electronVersion.value = await electron.getElectronVersion()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -177,8 +177,7 @@ export default {
|
||||
},
|
||||
|
||||
textColor: {
|
||||
muted: 'var(--p-text-muted-color)',
|
||||
highlight: 'var(--p-primary-color)'
|
||||
muted: 'var(--p-text-muted-color)'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -170,116 +170,20 @@ describe('useServerConfigStore', () => {
|
||||
const configs: ServerConfig<any>[] = [
|
||||
{ ...dummyFormItem, id: 'test.config1', defaultValue: 'default1' },
|
||||
{ ...dummyFormItem, id: 'test.config2', defaultValue: 'default2' },
|
||||
{ ...dummyFormItem, id: 'test.config3', defaultValue: 'default3' },
|
||||
{ ...dummyFormItem, id: 'test.config4', defaultValue: null }
|
||||
{ ...dummyFormItem, id: 'test.config3', defaultValue: 'default3' }
|
||||
]
|
||||
|
||||
store.loadServerConfig(configs, {
|
||||
'test.config1': undefined,
|
||||
'test.config2': null,
|
||||
'test.config3': '',
|
||||
'test.config4': 0
|
||||
'test.config3': ''
|
||||
})
|
||||
|
||||
expect(Object.keys(store.launchArgs)).toEqual([
|
||||
'test.config3',
|
||||
'test.config4'
|
||||
])
|
||||
expect(Object.values(store.launchArgs)).toEqual(['', '0'])
|
||||
expect(store.serverConfigById['test.config3'].value).toBe('')
|
||||
expect(store.serverConfigById['test.config4'].value).toBe(0)
|
||||
expect(Object.values(store.serverConfigValues)).toEqual([
|
||||
undefined,
|
||||
undefined,
|
||||
'',
|
||||
0
|
||||
])
|
||||
})
|
||||
|
||||
it('should convert true to empty string in launch arguments', () => {
|
||||
store.loadServerConfig(
|
||||
[
|
||||
{
|
||||
...dummyFormItem,
|
||||
id: 'test.config1',
|
||||
defaultValue: 0
|
||||
}
|
||||
],
|
||||
{
|
||||
'test.config1': true
|
||||
}
|
||||
)
|
||||
expect(store.launchArgs['test.config1']).toBe('')
|
||||
expect(store.commandLineArgs).toBe('--test.config1')
|
||||
})
|
||||
|
||||
it('should convert number to string in launch arguments', () => {
|
||||
store.loadServerConfig(
|
||||
[
|
||||
{
|
||||
...dummyFormItem,
|
||||
id: 'test.config1',
|
||||
defaultValue: 1
|
||||
}
|
||||
],
|
||||
{
|
||||
'test.config1': 123
|
||||
}
|
||||
)
|
||||
expect(store.launchArgs['test.config1']).toBe('123')
|
||||
expect(store.commandLineArgs).toBe('--test.config1 123')
|
||||
})
|
||||
|
||||
it('should drop nullish values in launch arguments', () => {
|
||||
store.loadServerConfig(
|
||||
[
|
||||
{
|
||||
...dummyFormItem,
|
||||
id: 'test.config1',
|
||||
defaultValue: 1
|
||||
}
|
||||
],
|
||||
{
|
||||
'test.config1': null
|
||||
}
|
||||
)
|
||||
expect(Object.keys(store.launchArgs)).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should track modified configs', () => {
|
||||
const configs = [
|
||||
{
|
||||
...dummyFormItem,
|
||||
id: 'test.config1',
|
||||
defaultValue: 'default1'
|
||||
},
|
||||
{
|
||||
...dummyFormItem,
|
||||
id: 'test.config2',
|
||||
defaultValue: 'default2'
|
||||
}
|
||||
]
|
||||
|
||||
store.loadServerConfig(configs, {
|
||||
'test.config1': 'initial1'
|
||||
})
|
||||
|
||||
// Initially no modified configs
|
||||
expect(store.modifiedConfigs).toHaveLength(0)
|
||||
|
||||
// Modify config1's value after loading
|
||||
store.serverConfigById['test.config1'].value = 'custom1'
|
||||
|
||||
// Now config1 should be in modified configs
|
||||
expect(store.modifiedConfigs).toHaveLength(1)
|
||||
expect(store.modifiedConfigs[0].id).toBe('test.config1')
|
||||
expect(store.modifiedConfigs[0].value).toBe('custom1')
|
||||
expect(store.modifiedConfigs[0].initialValue).toBe('initial1')
|
||||
|
||||
// Change config1 back to default
|
||||
store.serverConfigById['test.config1'].value = 'initial1'
|
||||
|
||||
// Should go back to no modified configs
|
||||
expect(store.modifiedConfigs).toHaveLength(0)
|
||||
expect(Object.keys(store.serverConfigValues)).toEqual([
|
||||
'test.config1',
|
||||
'test.config2',
|
||||
'test.config3'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -73,6 +73,7 @@ export async function checkBeforeAndAfterReload(graph, cb) {
|
||||
* @param { string } name
|
||||
* @param { Record<string, string | [string | string[], any]> } input
|
||||
* @param { (string | string[])[] | Record<string, string | string[]> } output
|
||||
* @returns { Record<string, import("../../src/types/comfy").ComfyObjectInfo> }
|
||||
*/
|
||||
export function makeNodeDef(name, input, output = {}) {
|
||||
const nodeDef = {
|
||||
|
||||
@@ -29,10 +29,14 @@ export interface APIConfig {
|
||||
userData?: Record<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef { import("../../src/types/comfy").ComfyObjectInfo } ComfyObjectInfo
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* mockExtensions?: string[],
|
||||
* mockNodeDefs?: Record<string, any>,
|
||||
* mockNodeDefs?: Record<string, ComfyObjectInfo>,
|
||||
* settings?: Record<string, string>
|
||||
* userConfig?: {storage: "server" | "browser", users?: Record<string, any>, migrated?: boolean },
|
||||
* userData?: Record<string, any>
|
||||
|
||||
@@ -10,9 +10,6 @@ const mockElectronAPI: Plugin = {
|
||||
{
|
||||
tag: 'script',
|
||||
children: `window.electronAPI = {
|
||||
restartApp: () => {
|
||||
alert('restartApp')
|
||||
},
|
||||
sendReady: () => {},
|
||||
onShowSelectDirectory: () => {},
|
||||
onFirstTimeSetupComplete: () => {},
|
||||
|
||||
Reference in New Issue
Block a user