Compare commits
23 Commits
v1.32.8
...
live-previ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee4599368c | ||
|
|
c781421cad | ||
|
|
bdf6d4dea2 | ||
|
|
b8a796212c | ||
|
|
bc553f12be | ||
|
|
6bb35d46c1 | ||
|
|
68c38f0098 | ||
|
|
236247f05f | ||
|
|
87d6d18c57 | ||
|
|
87106ccb95 | ||
|
|
a20fb7d260 | ||
|
|
836cd7f9ba | ||
|
|
acd855601c | ||
|
|
423a2e76bc | ||
|
|
26578981d4 | ||
|
|
38fb53dca8 | ||
|
|
a832141a45 | ||
|
|
d1f0211b61 | ||
|
|
cc42c2967c | ||
|
|
bb51a5aa76 | ||
|
|
674d884e79 | ||
|
|
6f89d9a9f8 | ||
|
|
08b206f191 |
@@ -16,7 +16,6 @@ import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
|
||||
import { SettingDialog } from './components/SettingDialog'
|
||||
import {
|
||||
NodeLibrarySidebarTab,
|
||||
QueueSidebarTab,
|
||||
WorkflowsSidebarTab
|
||||
} from './components/SidebarTab'
|
||||
import { Topbar } from './components/Topbar'
|
||||
@@ -31,7 +30,6 @@ type WorkspaceStore = ReturnType<typeof useWorkspaceStore>
|
||||
class ComfyMenu {
|
||||
private _nodeLibraryTab: NodeLibrarySidebarTab | null = null
|
||||
private _workflowsTab: WorkflowsSidebarTab | null = null
|
||||
private _queueTab: QueueSidebarTab | null = null
|
||||
private _topbar: Topbar | null = null
|
||||
|
||||
public readonly sideToolbar: Locator
|
||||
@@ -60,11 +58,6 @@ class ComfyMenu {
|
||||
return this._workflowsTab
|
||||
}
|
||||
|
||||
get queueTab() {
|
||||
this._queueTab ??= new QueueSidebarTab(this.page)
|
||||
return this._queueTab
|
||||
}
|
||||
|
||||
get topbar() {
|
||||
this._topbar ??= new Topbar(this.page)
|
||||
return this._topbar
|
||||
@@ -564,7 +557,7 @@ export class ComfyPage {
|
||||
async dragAndDrop(source: Position, target: Position) {
|
||||
await this.page.mouse.move(source.x, source.y)
|
||||
await this.page.mouse.down()
|
||||
await this.page.mouse.move(target.x, target.y, { steps: 100 })
|
||||
await this.page.mouse.move(target.x, target.y)
|
||||
await this.page.mouse.up()
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
@@ -65,9 +65,7 @@ export class VueNodeHelpers {
|
||||
* Select a specific Vue node by ID
|
||||
*/
|
||||
async selectNode(nodeId: string): Promise<void> {
|
||||
await this.page
|
||||
.locator(`[data-node-id="${nodeId}"] .lg-node-header`)
|
||||
.click()
|
||||
await this.page.locator(`[data-node-id="${nodeId}"]`).click()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,13 +77,11 @@ export class VueNodeHelpers {
|
||||
// Select first node normally
|
||||
await this.selectNode(nodeIds[0])
|
||||
|
||||
// Add additional nodes with Ctrl+click on header
|
||||
// Add additional nodes with Ctrl+click
|
||||
for (let i = 1; i < nodeIds.length; i++) {
|
||||
await this.page
|
||||
.locator(`[data-node-id="${nodeIds[i]}"] .lg-node-header`)
|
||||
.click({
|
||||
modifiers: ['Control']
|
||||
})
|
||||
await this.page.locator(`[data-node-id="${nodeIds[i]}"]`).click({
|
||||
modifiers: ['Control']
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -148,124 +148,3 @@ export class WorkflowsSidebarTab extends SidebarTab {
|
||||
.click()
|
||||
}
|
||||
}
|
||||
|
||||
export class QueueSidebarTab extends SidebarTab {
|
||||
constructor(public readonly page: Page) {
|
||||
super(page, 'queue')
|
||||
}
|
||||
|
||||
get root() {
|
||||
return this.page.locator('.sidebar-content-container', { hasText: 'Queue' })
|
||||
}
|
||||
|
||||
get tasks() {
|
||||
return this.root.locator('[data-virtual-grid-item]')
|
||||
}
|
||||
|
||||
get visibleTasks() {
|
||||
return this.tasks.locator('visible=true')
|
||||
}
|
||||
|
||||
get clearButton() {
|
||||
return this.root.locator('.clear-all-button')
|
||||
}
|
||||
|
||||
get collapseTasksButton() {
|
||||
return this.getToggleExpandButton(false)
|
||||
}
|
||||
|
||||
get expandTasksButton() {
|
||||
return this.getToggleExpandButton(true)
|
||||
}
|
||||
|
||||
get noResultsPlaceholder() {
|
||||
return this.root.locator('.no-results-placeholder')
|
||||
}
|
||||
|
||||
get galleryImage() {
|
||||
return this.page.locator('.galleria-image')
|
||||
}
|
||||
|
||||
private getToggleExpandButton(isExpanded: boolean) {
|
||||
const iconSelector = isExpanded ? '.pi-image' : '.pi-images'
|
||||
return this.root.locator(`.toggle-expanded-button ${iconSelector}`)
|
||||
}
|
||||
|
||||
async open() {
|
||||
await super.open()
|
||||
return this.root.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
async close() {
|
||||
await super.close()
|
||||
await this.root.waitFor({ state: 'hidden' })
|
||||
}
|
||||
|
||||
async expandTasks() {
|
||||
await this.expandTasksButton.click()
|
||||
await this.collapseTasksButton.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
async collapseTasks() {
|
||||
await this.collapseTasksButton.click()
|
||||
await this.expandTasksButton.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
async waitForTasks() {
|
||||
return Promise.all([
|
||||
this.tasks.first().waitFor({ state: 'visible' }),
|
||||
this.tasks.last().waitFor({ state: 'visible' })
|
||||
])
|
||||
}
|
||||
|
||||
async scrollTasks(direction: 'up' | 'down') {
|
||||
const scrollToEl =
|
||||
direction === 'up' ? this.tasks.last() : this.tasks.first()
|
||||
await scrollToEl.scrollIntoViewIfNeeded()
|
||||
await this.waitForTasks()
|
||||
}
|
||||
|
||||
async clearTasks() {
|
||||
await this.clearButton.click()
|
||||
const confirmButton = this.page.getByLabel('Delete')
|
||||
await confirmButton.click()
|
||||
await this.noResultsPlaceholder.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
/** Set the width of the tab (out of 100). Must call before opening the tab */
|
||||
async setTabWidth(width: number) {
|
||||
if (width < 0 || width > 100) {
|
||||
throw new Error('Width must be between 0 and 100')
|
||||
}
|
||||
return this.page.evaluate((width) => {
|
||||
localStorage.setItem('queue', JSON.stringify([width, 100 - width]))
|
||||
}, width)
|
||||
}
|
||||
|
||||
getTaskPreviewButton(taskIndex: number) {
|
||||
return this.tasks.nth(taskIndex).getByRole('button')
|
||||
}
|
||||
|
||||
async openTaskPreview(taskIndex: number) {
|
||||
const previewButton = this.getTaskPreviewButton(taskIndex)
|
||||
await previewButton.click()
|
||||
return this.galleryImage.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
getGalleryImage(imageFilename: string) {
|
||||
return this.galleryImage.and(this.page.getByAltText(imageFilename))
|
||||
}
|
||||
|
||||
getTaskImage(imageFilename: string) {
|
||||
return this.tasks.getByAltText(imageFilename)
|
||||
}
|
||||
|
||||
/** Trigger the queue store and tasks to update */
|
||||
async triggerTasksUpdate() {
|
||||
await this.page.evaluate(() => {
|
||||
window['app']['api'].dispatchCustomEvent('status', {
|
||||
exec_info: { queue_remaining: 0 }
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 96 KiB |
@@ -1,210 +0,0 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
|
||||
|
||||
test.describe.skip('Queue sidebar', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
|
||||
test('can display tasks', async ({ comfyPage }) => {
|
||||
await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1)
|
||||
})
|
||||
|
||||
test('can display tasks after closing then opening', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.close()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1)
|
||||
})
|
||||
|
||||
test.describe('Virtual scroll', () => {
|
||||
const layouts = [
|
||||
{ description: 'Five columns layout', width: 95, rows: 3, cols: 5 },
|
||||
{ description: 'Three columns layout', width: 55, rows: 3, cols: 3 },
|
||||
{ description: 'Two columns layout', width: 40, rows: 3, cols: 2 }
|
||||
]
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage
|
||||
.setupHistory()
|
||||
.withTask(['example.webp'])
|
||||
.repeat(50)
|
||||
.setupRoutes()
|
||||
})
|
||||
|
||||
layouts.forEach(({ description, width, rows, cols }) => {
|
||||
const preRenderedRows = 1
|
||||
const preRenderedTasks = preRenderedRows * cols * 2
|
||||
const visibleTasks = rows * cols
|
||||
const expectRenderLimit = visibleTasks + preRenderedTasks
|
||||
|
||||
test.describe(description, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.menu.queueTab.setTabWidth(width)
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
})
|
||||
|
||||
test('should not render items outside of view', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const renderedCount =
|
||||
await comfyPage.menu.queueTab.visibleTasks.count()
|
||||
expect(renderedCount).toBeLessThanOrEqual(expectRenderLimit)
|
||||
})
|
||||
|
||||
test('should teardown items after scrolling away', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.menu.queueTab.scrollTasks('down')
|
||||
const renderedCount =
|
||||
await comfyPage.menu.queueTab.visibleTasks.count()
|
||||
expect(renderedCount).toBeLessThanOrEqual(expectRenderLimit)
|
||||
})
|
||||
|
||||
test('should re-render items after scrolling away then back', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.menu.queueTab.scrollTasks('down')
|
||||
await comfyPage.menu.queueTab.scrollTasks('up')
|
||||
const renderedCount =
|
||||
await comfyPage.menu.queueTab.visibleTasks.count()
|
||||
expect(renderedCount).toBeLessThanOrEqual(expectRenderLimit)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Expand tasks', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
// 2-item batch and 3-item batch -> 3 additional items when expanded
|
||||
await comfyPage
|
||||
.setupHistory()
|
||||
.withTask(['example.webp', 'example.webp', 'example.webp'])
|
||||
.withTask(['example.webp', 'example.webp'])
|
||||
.setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
})
|
||||
|
||||
test('can expand tasks with multiple outputs', async ({ comfyPage }) => {
|
||||
const initialCount = await comfyPage.menu.queueTab.visibleTasks.count()
|
||||
await comfyPage.menu.queueTab.expandTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(
|
||||
initialCount + 3
|
||||
)
|
||||
})
|
||||
|
||||
test('can collapse flat tasks', async ({ comfyPage }) => {
|
||||
const initialCount = await comfyPage.menu.queueTab.visibleTasks.count()
|
||||
await comfyPage.menu.queueTab.expandTasks()
|
||||
await comfyPage.menu.queueTab.collapseTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(
|
||||
initialCount
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Clear tasks', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage
|
||||
.setupHistory()
|
||||
.withTask(['example.webp'])
|
||||
.repeat(6)
|
||||
.setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
})
|
||||
|
||||
test('can clear all tasks', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.queueTab.clearTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(0)
|
||||
expect(
|
||||
await comfyPage.menu.queueTab.noResultsPlaceholder.isVisible()
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('can load new tasks after clearing all', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.queueTab.clearTasks()
|
||||
await comfyPage.menu.queueTab.close()
|
||||
await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Gallery', () => {
|
||||
const firstImage = 'example.webp'
|
||||
const secondImage = 'image32x32.webp'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage
|
||||
.setupHistory()
|
||||
.withTask([secondImage])
|
||||
.withTask([firstImage])
|
||||
.setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
await comfyPage.menu.queueTab.openTaskPreview(0)
|
||||
})
|
||||
|
||||
test('displays gallery image after opening task preview', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.nextFrame()
|
||||
await expect(
|
||||
comfyPage.menu.queueTab.getGalleryImage(firstImage)
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('maintains active gallery item when new tasks are added', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Add a new task while the gallery is still open
|
||||
const newImage = 'image64x64.webp'
|
||||
comfyPage.setupHistory().withTask([newImage])
|
||||
await comfyPage.menu.queueTab.triggerTasksUpdate()
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
const newTask = comfyPage.menu.queueTab.tasks.getByAltText(newImage)
|
||||
await newTask.waitFor({ state: 'visible' })
|
||||
// The active gallery item should still be the initial image
|
||||
await expect(
|
||||
comfyPage.menu.queueTab.getGalleryImage(firstImage)
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test.describe('Gallery navigation', () => {
|
||||
const paths: {
|
||||
description: string
|
||||
path: ('Right' | 'Left')[]
|
||||
end: string
|
||||
}[] = [
|
||||
{ description: 'Right', path: ['Right'], end: secondImage },
|
||||
{ description: 'Left', path: ['Right', 'Left'], end: firstImage },
|
||||
{ description: 'Left wrap', path: ['Left'], end: secondImage },
|
||||
{ description: 'Right wrap', path: ['Right', 'Right'], end: firstImage }
|
||||
]
|
||||
|
||||
paths.forEach(({ description, path, end }) => {
|
||||
test(`can navigate gallery ${description}`, async ({ comfyPage }) => {
|
||||
for (const direction of path)
|
||||
await comfyPage.page.keyboard.press(`Arrow${direction}`, {
|
||||
delay: 256
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
await expect(
|
||||
comfyPage.menu.queueTab.getGalleryImage(end)
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.32.8",
|
||||
"version": "1.33.4",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -32,46 +32,12 @@
|
||||
</template>
|
||||
</SplitButton>
|
||||
<BatchCountEdit />
|
||||
<ButtonGroup class="execution-actions flex flex-nowrap">
|
||||
<Button
|
||||
v-tooltip.bottom="{
|
||||
value: $t('menu.interrupt'),
|
||||
showDelay: 600
|
||||
}"
|
||||
icon="pi pi-times"
|
||||
:severity="executingPrompt ? 'danger' : 'secondary'"
|
||||
:disabled="!executingPrompt"
|
||||
text
|
||||
:aria-label="$t('menu.interrupt')"
|
||||
@click="() => commandStore.execute('Comfy.Interrupt')"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.bottom="{
|
||||
value: $t('sideToolbar.queueTab.clearPendingTasks'),
|
||||
showDelay: 600
|
||||
}"
|
||||
icon="pi pi-stop"
|
||||
:severity="hasPendingTasks ? 'danger' : 'secondary'"
|
||||
:disabled="!hasPendingTasks"
|
||||
text
|
||||
:aria-label="$t('sideToolbar.queueTab.clearPendingTasks')"
|
||||
@click="
|
||||
() => {
|
||||
if (queueCountStore.count.value > 1) {
|
||||
commandStore.execute('Comfy.ClearPendingTasks')
|
||||
}
|
||||
queueMode = 'disabled'
|
||||
}
|
||||
"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Button from 'primevue/button'
|
||||
import ButtonGroup from 'primevue/buttongroup'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import SplitButton from 'primevue/splitbutton'
|
||||
import { computed } from 'vue'
|
||||
@@ -80,17 +46,13 @@ import { useI18n } from 'vue-i18n'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import {
|
||||
useQueuePendingTaskCountStore,
|
||||
useQueueSettingsStore
|
||||
} from '@/stores/queueStore'
|
||||
import { useQueueSettingsStore } from '@/stores/queueStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
||||
|
||||
import BatchCountEdit from '../BatchCountEdit.vue'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
|
||||
const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())
|
||||
|
||||
const { hasMissingNodes } = useMissingNodes()
|
||||
@@ -145,11 +107,6 @@ const queueModeMenuItems = computed(() =>
|
||||
Object.values(queueModeMenuItemLookup.value)
|
||||
)
|
||||
|
||||
const executingPrompt = computed(() => !!queueCountStore.count.value)
|
||||
const hasPendingTasks = computed(
|
||||
() => queueCountStore.count.value > 1 || queueMode.value !== 'disabled'
|
||||
)
|
||||
|
||||
const iconClass = computed(() => {
|
||||
if (hasMissingNodes.value) {
|
||||
return 'icon-[lucide--triangle-alert]'
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
interface IconButtonProps extends BaseButtonProps {
|
||||
onClick?: (event: MouseEvent) => void
|
||||
onClick: (event: Event) => void
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
|
||||
@@ -47,7 +47,7 @@ const {
|
||||
} = defineProps<IconTextButtonProps>()
|
||||
|
||||
const buttonStyle = computed(() => {
|
||||
const baseClasses = `${getBaseButtonClasses()} justify-start gap-2`
|
||||
const baseClasses = `${getBaseButtonClasses()} justify-start! gap-2`
|
||||
const sizeClasses = getButtonSizeClasses(size)
|
||||
const typeClasses = border
|
||||
? getBorderButtonTypeClasses(type)
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
class="w-62.5"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--arrow-up-down] text-muted-foreground" />
|
||||
<i class="icon-[lucide--arrow-up-down]" />
|
||||
</template>
|
||||
</SingleSelect>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<InputText
|
||||
ref="inputRef"
|
||||
v-model="inputValue"
|
||||
:placeholder
|
||||
autofocus
|
||||
@keyup.enter="onConfirm"
|
||||
@focus="selectAllText"
|
||||
@@ -28,6 +29,7 @@ const props = defineProps<{
|
||||
message: string
|
||||
defaultValue: string
|
||||
onConfirm: (value: string) => void
|
||||
placeholder?: string
|
||||
}>()
|
||||
|
||||
const inputValue = ref<string>(props.defaultValue)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
root: ({ props }: MultiSelectPassThroughMethodOptions) => ({
|
||||
class: cn(
|
||||
'h-10 relative inline-flex cursor-pointer select-none',
|
||||
'rounded-lg bg-secondary-background text-base-foreground',
|
||||
'rounded-lg bg-base-background text-base-foreground',
|
||||
'transition-all duration-200 ease-in-out',
|
||||
'border-[2.5px] border-solid',
|
||||
selectedCount > 0
|
||||
@@ -127,7 +127,7 @@
|
||||
|
||||
<!-- Trigger value (keep text scale identical) -->
|
||||
<template #value>
|
||||
<span class="text-sm">
|
||||
<span class="text-sm text-muted-foreground">
|
||||
{{ label }}
|
||||
</span>
|
||||
<span
|
||||
@@ -140,7 +140,7 @@
|
||||
|
||||
<!-- Chevron size identical to current -->
|
||||
<template #dropdownicon>
|
||||
<i class="icon-[lucide--chevron-down] text-muted-foreground" />
|
||||
<i class="icon-[lucide--chevron-down] text-lg text-neutral-400" />
|
||||
</template>
|
||||
|
||||
<!-- Custom option row: square checkbox + label (unchanged layout/colors) -->
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div :class="wrapperStyle" @click="focusInput">
|
||||
<i class="icon-[lucide--search] text-muted-foreground" />
|
||||
<i class="icon-[lucide--search] text-muted" />
|
||||
<InputText
|
||||
ref="input"
|
||||
v-model="internalSearchQuery"
|
||||
@@ -73,7 +73,7 @@ onMounted(() => autofocus && focusInput())
|
||||
|
||||
const wrapperStyle = computed(() => {
|
||||
const baseClasses =
|
||||
'relative flex w-full items-center gap-2 bg-secondary-background cursor-text'
|
||||
'relative flex w-full items-center gap-2 bg-base-background cursor-text'
|
||||
|
||||
if (showBorder) {
|
||||
return cn(
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
'h-10 relative inline-flex cursor-pointer select-none items-center',
|
||||
// trigger surface
|
||||
'rounded-lg',
|
||||
'bg-secondary-background text-base-foreground',
|
||||
'bg-base-background text-base-foreground',
|
||||
'border-[2.5px] border-solid border-transparent',
|
||||
'transition-all duration-200 ease-in-out',
|
||||
'focus-within:border-node-component-border',
|
||||
@@ -84,7 +84,7 @@
|
||||
>
|
||||
<!-- Trigger value -->
|
||||
<template #value="slotProps">
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<div class="flex items-center gap-2 text-sm text-neutral-500">
|
||||
<slot name="icon" />
|
||||
<span
|
||||
v-if="slotProps.value !== null && slotProps.value !== undefined"
|
||||
@@ -100,7 +100,7 @@
|
||||
|
||||
<!-- Trigger caret -->
|
||||
<template #dropdownicon>
|
||||
<i class="icon-[lucide--chevron-down] text-muted-foreground" />
|
||||
<i class="icon-[lucide--chevron-down] text-base text-neutral-500" />
|
||||
</template>
|
||||
|
||||
<!-- Option row -->
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<template>
|
||||
<IconButton
|
||||
type="secondary"
|
||||
size="fit-content"
|
||||
class="group w-full justify-between gap-3 rounded-lg p-1 text-left font-normal hover:cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
|
||||
<button
|
||||
type="button"
|
||||
class="group flex w-full items-center justify-between gap-3 rounded-lg border-0 bg-secondary-background p-1 text-left transition-colors duration-200 ease-in-out hover:cursor-pointer hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
|
||||
:aria-label="props.ariaLabel"
|
||||
@click="emit('click', $event)"
|
||||
>
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<span v-if="props.mode === 'allFailed'" class="inline-flex items-center">
|
||||
@@ -78,11 +76,10 @@
|
||||
>
|
||||
<i class="icon-[lucide--chevron-down] block size-4 leading-none" />
|
||||
</span>
|
||||
</IconButton>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import type {
|
||||
CompletionSummary,
|
||||
CompletionSummaryMode
|
||||
@@ -99,8 +96,4 @@ type Props = {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
thumbnailUrls: () => []
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'click', event: MouseEvent): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -42,19 +42,17 @@
|
||||
t('sideToolbar.queueProgressOverlay.running')
|
||||
}}</span>
|
||||
</span>
|
||||
<IconButton
|
||||
<button
|
||||
v-if="runningCount > 0"
|
||||
v-tooltip.top="cancelJobTooltip"
|
||||
type="secondary"
|
||||
size="sm"
|
||||
class="size-6 bg-secondary-background hover:bg-destructive-background"
|
||||
class="inline-flex size-6 cursor-pointer items-center justify-center rounded border-0 bg-secondary-background p-0 transition-colors hover:bg-destructive-background"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.interruptAll')"
|
||||
@click="$emit('interruptAll')"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--x] block size-4 leading-none text-text-primary"
|
||||
/>
|
||||
</IconButton>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -64,28 +62,26 @@
|
||||
t('sideToolbar.queueProgressOverlay.queuedSuffix')
|
||||
}}</span>
|
||||
</span>
|
||||
<IconButton
|
||||
<button
|
||||
v-if="queuedCount > 0"
|
||||
v-tooltip.top="clearQueueTooltip"
|
||||
type="secondary"
|
||||
size="sm"
|
||||
class="size-6 bg-secondary-background hover:bg-destructive-background"
|
||||
class="inline-flex size-6 cursor-pointer items-center justify-center rounded border-0 bg-secondary-background p-0 transition-colors hover:bg-destructive-background"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.clearQueued')"
|
||||
@click="$emit('clearQueued')"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--list-x] block size-4 leading-none text-text-primary"
|
||||
/>
|
||||
</IconButton>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TextButton
|
||||
class="h-6 min-w-[120px] flex-1 px-2 py-0 text-[12px]"
|
||||
type="secondary"
|
||||
:label="t('sideToolbar.queueProgressOverlay.viewAllJobs')"
|
||||
<button
|
||||
class="inline-flex h-6 min-w-[120px] flex-1 cursor-pointer items-center justify-center rounded border-0 bg-secondary-background px-2 py-0 text-[12px] text-text-primary hover:bg-secondary-background-hover hover:opacity-90"
|
||||
@click="$emit('viewAllJobs')"
|
||||
/>
|
||||
>
|
||||
{{ t('sideToolbar.queueProgressOverlay.viewAllJobs') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -94,8 +90,6 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import TextButton from '@/components/button/TextButton.vue'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
|
||||
defineProps<{
|
||||
|
||||
@@ -8,20 +8,17 @@
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-between px-3">
|
||||
<IconTextButton
|
||||
class="grow gap-1 p-2 text-center font-inter text-[12px] leading-none hover:opacity-90 justify-center"
|
||||
type="secondary"
|
||||
:label="t('sideToolbar.queueProgressOverlay.showAssets')"
|
||||
<button
|
||||
class="inline-flex grow cursor-pointer items-center justify-center gap-1 rounded border-0 bg-secondary-background p-2 text-center font-inter text-[12px] leading-none text-text-primary hover:bg-secondary-background-hover hover:opacity-90"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.showAssets')"
|
||||
@click="$emit('showAssets')"
|
||||
>
|
||||
<template #icon>
|
||||
<div
|
||||
class="pointer-events-none block size-4 shrink-0 leading-none icon-[comfy--image-ai-edit]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<div
|
||||
class="pointer-events-none block size-4 shrink-0 leading-none icon-[comfy--image-ai-edit]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{{ t('sideToolbar.queueProgressOverlay.showAssets') }}</span>
|
||||
</button>
|
||||
<div class="ml-4 inline-flex items-center">
|
||||
<div
|
||||
class="inline-flex h-6 items-center text-[12px] leading-none text-text-primary opacity-90"
|
||||
@@ -31,18 +28,16 @@
|
||||
t('sideToolbar.queueProgressOverlay.queuedSuffix')
|
||||
}}</span>
|
||||
</div>
|
||||
<IconButton
|
||||
<button
|
||||
v-if="queuedCount > 0"
|
||||
class="group ml-2 size-6 bg-secondary-background hover:bg-destructive-background"
|
||||
type="secondary"
|
||||
size="sm"
|
||||
class="group ml-2 inline-flex size-6 cursor-pointer items-center justify-center rounded border-0 bg-secondary-background p-0 transition-colors hover:bg-destructive-background"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.clearQueued')"
|
||||
@click="$emit('clearQueued')"
|
||||
>
|
||||
<i
|
||||
class="pointer-events-none icon-[lucide--list-x] block size-4 leading-none text-text-primary transition-colors group-hover:text-base-background"
|
||||
/>
|
||||
</IconButton>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -80,8 +75,6 @@
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import type {
|
||||
JobGroup,
|
||||
JobListItem,
|
||||
|
||||
@@ -18,18 +18,16 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<IconButton
|
||||
<button
|
||||
v-tooltip.top="moreTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="size-6 bg-transparent hover:bg-secondary-background hover:opacity-100"
|
||||
class="inline-flex size-6 cursor-pointer items-center justify-center rounded border-0 bg-transparent p-0 hover:bg-secondary-background hover:opacity-100"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.moreOptions')"
|
||||
@click="onMoreClick"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--more-horizontal] block size-4 leading-none text-text-secondary"
|
||||
/>
|
||||
</IconButton>
|
||||
</button>
|
||||
<Popover
|
||||
ref="morePopoverRef"
|
||||
:dismissable="true"
|
||||
@@ -47,19 +45,18 @@
|
||||
<div
|
||||
class="flex flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3 font-inter"
|
||||
>
|
||||
<IconTextButton
|
||||
class="w-full justify-start gap-2 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
|
||||
type="transparent"
|
||||
:label="t('sideToolbar.queueProgressOverlay.clearHistory')"
|
||||
<button
|
||||
class="inline-flex w-full cursor-pointer items-center justify-start gap-2 rounded-lg border-0 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.clearHistory')"
|
||||
@click="onClearHistoryFromMenu"
|
||||
>
|
||||
<template #icon>
|
||||
<i
|
||||
class="icon-[lucide--file-x-2] block size-4 leading-none text-text-secondary"
|
||||
/>
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<i
|
||||
class="icon-[lucide--file-x-2] block size-4 leading-none text-text-secondary"
|
||||
/>
|
||||
<span>{{
|
||||
t('sideToolbar.queueProgressOverlay.clearHistory')
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
@@ -72,8 +69,6 @@ import type { PopoverMethods } from 'primevue/popover'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
|
||||
defineProps<{
|
||||
|
||||
@@ -8,15 +8,13 @@
|
||||
<p class="m-0 text-[14px] font-normal leading-none">
|
||||
{{ t('sideToolbar.queueProgressOverlay.clearHistoryDialogTitle') }}
|
||||
</p>
|
||||
<IconButton
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="size-6 bg-transparent text-text-secondary hover:bg-secondary-background hover:opacity-100"
|
||||
<button
|
||||
class="inline-flex size-6 cursor-pointer items-center justify-center rounded border-0 bg-transparent p-0 text-text-secondary transition hover:bg-secondary-background hover:opacity-100"
|
||||
:aria-label="t('g.close')"
|
||||
@click="onCancel"
|
||||
>
|
||||
<i class="icon-[lucide--x] block size-4 leading-none" />
|
||||
</IconButton>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="flex flex-col gap-4 px-4 py-4 text-[14px] text-text-secondary">
|
||||
@@ -32,19 +30,21 @@
|
||||
|
||||
<footer class="flex items-center justify-end px-4 py-4">
|
||||
<div class="flex items-center gap-4 text-[14px] leading-none">
|
||||
<TextButton
|
||||
class="min-h-[24px] px-1 py-1 text-[14px] leading-[1] text-text-secondary hover:text-text-primary"
|
||||
type="transparent"
|
||||
:label="t('g.cancel')"
|
||||
<button
|
||||
class="inline-flex min-h-[24px] cursor-pointer items-center rounded-md border-0 bg-transparent px-1 py-1 text-[14px] leading-[1] text-text-secondary transition hover:text-text-primary"
|
||||
:aria-label="t('g.cancel')"
|
||||
@click="onCancel"
|
||||
/>
|
||||
<TextButton
|
||||
class="min-h-[32px] px-4 py-2 text-[12px] font-normal leading-[1]"
|
||||
type="secondary"
|
||||
:label="t('g.clear')"
|
||||
>
|
||||
{{ t('g.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex min-h-[32px] items-center rounded-lg border-0 bg-secondary-background px-4 py-2 text-[12px] font-normal leading-[1] text-text-primary transition hover:bg-secondary-background-hover hover:text-text-primary disabled:cursor-not-allowed disabled:opacity-60"
|
||||
:aria-label="t('g.clear')"
|
||||
:disabled="isClearing"
|
||||
@click="onConfirm"
|
||||
/>
|
||||
>
|
||||
{{ t('g.clear') }}
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</section>
|
||||
@@ -54,8 +54,6 @@
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import TextButton from '@/components/button/TextButton.vue'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
|
||||
@@ -20,24 +20,21 @@
|
||||
<div v-if="entry.kind === 'divider'" class="px-2 py-1">
|
||||
<div class="h-px bg-interface-stroke" />
|
||||
</div>
|
||||
<IconTextButton
|
||||
<button
|
||||
v-else
|
||||
class="w-full justify-start gap-2 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-interface-panel-hover-surface"
|
||||
type="transparent"
|
||||
:label="entry.label"
|
||||
class="inline-flex w-full cursor-pointer items-center justify-start gap-2 rounded-lg border-0 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary transition-colors duration-150 hover:bg-interface-panel-hover-surface"
|
||||
:aria-label="entry.label"
|
||||
@click="onEntry(entry)"
|
||||
>
|
||||
<template #icon>
|
||||
<i
|
||||
v-if="entry.icon"
|
||||
:class="[
|
||||
entry.icon,
|
||||
'block size-4 shrink-0 leading-none text-text-secondary'
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<i
|
||||
v-if="entry.icon"
|
||||
:class="[
|
||||
entry.icon,
|
||||
'block size-4 shrink-0 leading-none text-text-secondary'
|
||||
]"
|
||||
/>
|
||||
<span>{{ entry.label }}</span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</Popover>
|
||||
@@ -47,7 +44,6 @@
|
||||
import Popover from 'primevue/popover'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import type { MenuEntry } from '@/composables/queue/useJobMenu'
|
||||
|
||||
defineProps<{ entries: MenuEntry[] }>()
|
||||
|
||||
@@ -20,18 +20,17 @@
|
||||
class="flex min-w-0 items-center text-[0.75rem] leading-normal font-normal text-text-secondary"
|
||||
>
|
||||
<span class="block min-w-0 truncate">{{ row.value }}</span>
|
||||
<IconButton
|
||||
<button
|
||||
v-if="row.canCopy"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="ml-2 size-6 bg-transparent hover:opacity-90"
|
||||
type="button"
|
||||
class="ml-2 inline-flex size-6 items-center justify-center rounded border-0 bg-transparent p-0 hover:opacity-90"
|
||||
:aria-label="copyAriaLabel"
|
||||
@click.stop="copyJobId"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--copy] block size-4 leading-none text-text-secondary"
|
||||
/>
|
||||
</IconButton>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -61,31 +60,25 @@
|
||||
{{ t('queue.jobDetails.errorMessage') }}
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<IconTextButton
|
||||
class="h-6 justify-start gap-2 bg-transparent px-0 text-[0.75rem] leading-none text-text-secondary hover:opacity-90"
|
||||
type="transparent"
|
||||
:label="copyAriaLabel"
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-6 items-center justify-center gap-2 rounded border-none bg-transparent px-0 text-[0.75rem] leading-none text-text-secondary hover:opacity-90"
|
||||
:aria-label="copyAriaLabel"
|
||||
icon-position="right"
|
||||
@click.stop="copyErrorMessage"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--copy] block size-3.5 leading-none" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<IconTextButton
|
||||
class="h-6 justify-start gap-2 bg-transparent px-0 text-[0.75rem] leading-none text-text-secondary hover:opacity-90"
|
||||
type="transparent"
|
||||
:label="t('queue.jobDetails.report')"
|
||||
icon-position="right"
|
||||
<span>{{ copyAriaLabel }}</span>
|
||||
<i class="icon-[lucide--copy] block size-3.5 leading-none" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-6 items-center justify-center gap-2 rounded border-none bg-transparent px-0 text-[0.75rem] leading-none text-text-secondary hover:opacity-90"
|
||||
@click.stop="reportJobError"
|
||||
>
|
||||
<template #icon>
|
||||
<i
|
||||
class="icon-[lucide--message-circle-warning] block size-3.5 leading-none"
|
||||
/>
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<span>{{ t('queue.jobDetails.report') }}</span>
|
||||
<i
|
||||
class="icon-[lucide--message-circle-warning] block size-3.5 leading-none"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="col-span-2 mt-2 rounded bg-interface-panel-hover-surface px-4 py-2 text-[0.75rem] leading-normal text-text-secondary"
|
||||
@@ -101,8 +94,6 @@
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import { t } from '@/i18n'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
|
||||
@@ -2,26 +2,26 @@
|
||||
<div class="flex items-center justify-between gap-2 px-3">
|
||||
<div class="min-w-0 flex-1 overflow-x-auto">
|
||||
<div class="inline-flex items-center gap-1 whitespace-nowrap">
|
||||
<TextButton
|
||||
<button
|
||||
v-for="tab in visibleJobTabs"
|
||||
:key="tab"
|
||||
class="h-6 px-3 py-1 text-[12px] leading-none hover:opacity-90"
|
||||
:type="selectedJobTab === tab ? 'secondary' : 'transparent'"
|
||||
class="h-6 cursor-pointer rounded border-0 px-3 py-1 text-[12px] leading-none hover:opacity-90"
|
||||
:class="[
|
||||
selectedJobTab === tab ? 'text-text-primary' : 'text-text-secondary'
|
||||
selectedJobTab === tab
|
||||
? 'bg-secondary-background text-text-primary'
|
||||
: 'bg-transparent text-text-secondary'
|
||||
]"
|
||||
:label="tabLabel(tab)"
|
||||
@click="$emit('update:selectedJobTab', tab)"
|
||||
/>
|
||||
>
|
||||
{{ tabLabel(tab) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-2 flex shrink-0 items-center gap-2">
|
||||
<IconButton
|
||||
<button
|
||||
v-if="showWorkflowFilter"
|
||||
v-tooltip.top="filterTooltipConfig"
|
||||
type="secondary"
|
||||
size="sm"
|
||||
class="relative size-6 bg-secondary-background hover:bg-secondary-background-hover hover:opacity-90"
|
||||
class="relative inline-flex size-6 cursor-pointer items-center justify-center rounded border-0 bg-secondary-background p-0 hover:bg-secondary-background-hover hover:opacity-90"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.filterJobs')"
|
||||
@click="onFilterClick"
|
||||
>
|
||||
@@ -32,7 +32,7 @@
|
||||
v-if="selectedWorkflowFilter !== 'all'"
|
||||
class="pointer-events-none absolute -top-1 -right-1 inline-block size-2 rounded-full bg-base-foreground"
|
||||
/>
|
||||
</IconButton>
|
||||
</button>
|
||||
<Popover
|
||||
v-if="showWorkflowFilter"
|
||||
ref="filterPopoverRef"
|
||||
@@ -51,48 +51,46 @@
|
||||
<div
|
||||
class="flex min-w-[12rem] flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3"
|
||||
>
|
||||
<IconTextButton
|
||||
class="w-full justify-between gap-1 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
|
||||
type="transparent"
|
||||
icon-position="right"
|
||||
:label="t('sideToolbar.queueProgressOverlay.filterAllWorkflows')"
|
||||
<button
|
||||
class="inline-flex w-full cursor-pointer items-center justify-start gap-1 rounded-lg border-0 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
|
||||
:aria-label="
|
||||
t('sideToolbar.queueProgressOverlay.filterAllWorkflows')
|
||||
"
|
||||
@click="selectWorkflowFilter('all')"
|
||||
>
|
||||
<template #icon>
|
||||
<span>{{
|
||||
t('sideToolbar.queueProgressOverlay.filterAllWorkflows')
|
||||
}}</span>
|
||||
<span class="ml-auto inline-flex items-center">
|
||||
<i
|
||||
v-if="selectedWorkflowFilter === 'all'"
|
||||
class="icon-[lucide--check] block size-4 leading-none text-text-secondary"
|
||||
/>
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</span>
|
||||
</button>
|
||||
<div class="mx-2 mt-1 h-px" />
|
||||
<IconTextButton
|
||||
class="w-full justify-between gap-1 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
|
||||
type="transparent"
|
||||
icon-position="right"
|
||||
:label="t('sideToolbar.queueProgressOverlay.filterCurrentWorkflow')"
|
||||
<button
|
||||
class="inline-flex w-full cursor-pointer items-center justify-start gap-1 rounded-lg border-0 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
|
||||
:aria-label="
|
||||
t('sideToolbar.queueProgressOverlay.filterCurrentWorkflow')
|
||||
"
|
||||
@click="selectWorkflowFilter('current')"
|
||||
>
|
||||
<template #icon>
|
||||
<span>{{
|
||||
t('sideToolbar.queueProgressOverlay.filterCurrentWorkflow')
|
||||
}}</span>
|
||||
<span class="ml-auto inline-flex items-center">
|
||||
<i
|
||||
v-if="selectedWorkflowFilter === 'current'"
|
||||
class="icon-[lucide--check] block size-4 leading-none text-text-secondary"
|
||||
/>
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</Popover>
|
||||
<IconButton
|
||||
<button
|
||||
v-tooltip.top="sortTooltipConfig"
|
||||
type="secondary"
|
||||
size="sm"
|
||||
class="relative size-6 bg-secondary-background hover:bg-secondary-background-hover hover:opacity-90"
|
||||
class="relative inline-flex size-6 cursor-pointer items-center justify-center rounded border-0 bg-secondary-background p-0 hover:bg-secondary-background-hover hover:opacity-90"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.sortJobs')"
|
||||
@click="onSortClick"
|
||||
>
|
||||
@@ -103,7 +101,7 @@
|
||||
v-if="selectedSortMode !== 'mostRecent'"
|
||||
class="pointer-events-none absolute -top-1 -right-1 inline-block size-2 rounded-full bg-base-foreground"
|
||||
/>
|
||||
</IconButton>
|
||||
</button>
|
||||
<Popover
|
||||
ref="sortPopoverRef"
|
||||
:dismissable="true"
|
||||
@@ -122,21 +120,19 @@
|
||||
class="flex min-w-[12rem] flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3"
|
||||
>
|
||||
<template v-for="(mode, index) in jobSortModes" :key="mode">
|
||||
<IconTextButton
|
||||
class="w-full justify-between gap-1 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
|
||||
type="transparent"
|
||||
icon-position="right"
|
||||
:label="sortLabel(mode)"
|
||||
<button
|
||||
class="inline-flex w-full cursor-pointer items-center justify-start gap-1 rounded-lg border-0 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
|
||||
:aria-label="sortLabel(mode)"
|
||||
@click="selectSortMode(mode)"
|
||||
>
|
||||
<template #icon>
|
||||
<span>{{ sortLabel(mode) }}</span>
|
||||
<span class="ml-auto inline-flex items-center">
|
||||
<i
|
||||
v-if="selectedSortMode === mode"
|
||||
class="icon-[lucide--check] block size-4 leading-none text-text-secondary"
|
||||
/>
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
v-if="index < jobSortModes.length - 1"
|
||||
class="mx-2 mt-1 h-px"
|
||||
@@ -153,9 +149,6 @@ import Popover from 'primevue/popover'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import TextButton from '@/components/button/TextButton.vue'
|
||||
import { jobSortModes, jobTabs } from '@/composables/queue/useJobList'
|
||||
import type { JobSortMode, JobTab } from '@/composables/queue/useJobList'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
|
||||
@@ -108,47 +108,45 @@
|
||||
key="actions"
|
||||
class="inline-flex items-center gap-2 pr-1"
|
||||
>
|
||||
<IconButton
|
||||
<button
|
||||
v-if="props.state === 'failed' && computedShowClear"
|
||||
v-tooltip.top="deleteTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="h-6 transform gap-1 rounded bg-modal-card-button-surface px-1 py-0 text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background hover:opacity-95"
|
||||
type="button"
|
||||
class="inline-flex h-6 transform cursor-pointer items-center gap-1 rounded border-0 bg-modal-card-button-surface px-1 py-0 text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background hover:opacity-95"
|
||||
:aria-label="t('g.delete')"
|
||||
@click.stop="emit('delete')"
|
||||
>
|
||||
<i class="icon-[lucide--trash-2] size-4" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
</button>
|
||||
<button
|
||||
v-else-if="props.state !== 'completed' && computedShowClear"
|
||||
v-tooltip.top="cancelTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="h-6 transform gap-1 rounded bg-modal-card-button-surface px-1 py-0 text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background hover:opacity-95"
|
||||
type="button"
|
||||
class="inline-flex h-6 transform cursor-pointer items-center gap-1 rounded border-0 bg-modal-card-button-surface px-1 py-0 text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background hover:opacity-95"
|
||||
:aria-label="t('g.cancel')"
|
||||
@click.stop="emit('cancel')"
|
||||
>
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</IconButton>
|
||||
<TextButton
|
||||
</button>
|
||||
<button
|
||||
v-else-if="props.state === 'completed'"
|
||||
class="h-6 transform gap-1 rounded bg-modal-card-button-surface px-2 py-0 text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:opacity-95"
|
||||
type="transparent"
|
||||
:label="t('menuLabels.View')"
|
||||
type="button"
|
||||
class="inline-flex h-6 transform cursor-pointer items-center gap-1 rounded border-0 bg-modal-card-button-surface px-2 py-0 text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:opacity-95"
|
||||
:aria-label="t('menuLabels.View')"
|
||||
@click.stop="emit('view')"
|
||||
/>
|
||||
<IconButton
|
||||
>
|
||||
<span>{{ t('menuLabels.View') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="props.showMenu !== undefined ? props.showMenu : true"
|
||||
v-tooltip.top="moreTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="h-6 transform gap-1 rounded bg-modal-card-button-surface px-1 py-0 text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:opacity-95"
|
||||
type="button"
|
||||
class="inline-flex h-6 transform cursor-pointer items-center gap-1 rounded border-0 bg-modal-card-button-surface px-1 py-0 text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:opacity-95"
|
||||
:aria-label="t('g.more')"
|
||||
@click.stop="emit('menu', $event)"
|
||||
>
|
||||
<i class="icon-[lucide--more-horizontal] size-4" />
|
||||
</IconButton>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else key="secondary" class="pr-2">
|
||||
<slot name="secondary">{{ props.rightText }}</slot>
|
||||
@@ -163,8 +161,6 @@
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import TextButton from '@/components/button/TextButton.vue'
|
||||
import JobDetailsPopover from '@/components/queue/job/JobDetailsPopover.vue'
|
||||
import QueueAssetPreview from '@/components/queue/job/QueueAssetPreview.vue'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
|
||||
@@ -1,289 +0,0 @@
|
||||
<template>
|
||||
<SidebarTabTemplate :title="$t('sideToolbar.queue')">
|
||||
<template #tool-buttons>
|
||||
<Button
|
||||
v-tooltip.bottom="$t(`sideToolbar.queueTab.${imageFit}ImagePreview`)"
|
||||
:icon="
|
||||
imageFit === 'cover'
|
||||
? 'pi pi-arrow-down-left-and-arrow-up-right-to-center'
|
||||
: 'pi pi-arrow-up-right-and-arrow-down-left-from-center'
|
||||
"
|
||||
text
|
||||
severity="secondary"
|
||||
class="toggle-expanded-button"
|
||||
@click="toggleImageFit"
|
||||
/>
|
||||
<Button
|
||||
v-if="isInFolderView"
|
||||
v-tooltip.bottom="$t('sideToolbar.queueTab.backToAllTasks')"
|
||||
icon="pi pi-arrow-left"
|
||||
text
|
||||
severity="secondary"
|
||||
class="back-button"
|
||||
@click="exitFolderView"
|
||||
/>
|
||||
<template v-else>
|
||||
<Button
|
||||
v-tooltip="$t('sideToolbar.queueTab.showFlatList')"
|
||||
:icon="isExpanded ? 'pi pi-images' : 'pi pi-image'"
|
||||
text
|
||||
severity="secondary"
|
||||
class="toggle-expanded-button"
|
||||
@click="toggleExpanded"
|
||||
/>
|
||||
<Button
|
||||
v-if="queueStore.hasPendingTasks"
|
||||
v-tooltip.bottom="$t('sideToolbar.queueTab.clearPendingTasks')"
|
||||
icon="pi pi-stop"
|
||||
severity="danger"
|
||||
text
|
||||
@click="() => commandStore.execute('Comfy.ClearPendingTasks')"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-trash"
|
||||
text
|
||||
severity="primary"
|
||||
class="clear-all-button"
|
||||
@click="confirmRemoveAll($event)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<template #body>
|
||||
<VirtualGrid
|
||||
v-if="allTasks?.length"
|
||||
:items="allTasks"
|
||||
:grid-style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
|
||||
padding: '0.5rem',
|
||||
gap: '0.5rem'
|
||||
}"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<TaskItem
|
||||
:task="item"
|
||||
:is-flat-task="isExpanded || isInFolderView"
|
||||
@contextmenu="handleContextMenu"
|
||||
@preview="handlePreview"
|
||||
@task-output-length-clicked="enterFolderView($event)"
|
||||
/>
|
||||
</template>
|
||||
</VirtualGrid>
|
||||
<div v-else-if="queueStore.isLoading">
|
||||
<ProgressSpinner
|
||||
style="width: 50px; left: 50%; transform: translateX(-50%)"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<NoResultsPlaceholder
|
||||
icon="pi pi-info-circle"
|
||||
:title="$t('g.noTasksFound')"
|
||||
:message="$t('g.noTasksFoundMessage')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</SidebarTabTemplate>
|
||||
<ConfirmPopup />
|
||||
<ContextMenu ref="menu" :model="menuItems" />
|
||||
<ResultGallery
|
||||
v-model:active-index="galleryActiveIndex"
|
||||
:all-gallery-items="allGalleryItems"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import ConfirmPopup from 'primevue/confirmpopup'
|
||||
import ContextMenu from 'primevue/contextmenu'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { useConfirm } from 'primevue/useconfirm'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, ref, shallowRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import VirtualGrid from '@/components/common/VirtualGrid.vue'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { ComfyNode } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import type { ResultItemImpl, TaskItemImpl } from '@/stores/queueStore'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
|
||||
import SidebarTabTemplate from './SidebarTabTemplate.vue'
|
||||
import ResultGallery from './queue/ResultGallery.vue'
|
||||
import TaskItem from './queue/TaskItem.vue'
|
||||
|
||||
const IMAGE_FIT = 'Comfy.Queue.ImageFit'
|
||||
const confirm = useConfirm()
|
||||
const toast = useToast()
|
||||
const queueStore = useQueueStore()
|
||||
const settingStore = useSettingStore()
|
||||
const commandStore = useCommandStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
// Expanded view: show all outputs in a flat list.
|
||||
const isExpanded = ref(false)
|
||||
const galleryActiveIndex = ref(-1)
|
||||
const allGalleryItems = shallowRef<ResultItemImpl[]>([])
|
||||
// Folder view: only show outputs from a single selected task.
|
||||
const folderTask = ref<TaskItemImpl | null>(null)
|
||||
const isInFolderView = computed(() => folderTask.value !== null)
|
||||
const imageFit = computed<string>(() => settingStore.get(IMAGE_FIT))
|
||||
|
||||
const allTasks = computed(() =>
|
||||
isInFolderView.value
|
||||
? folderTask.value
|
||||
? folderTask.value.flatten()
|
||||
: []
|
||||
: isExpanded.value
|
||||
? queueStore.flatTasks
|
||||
: queueStore.tasks
|
||||
)
|
||||
const updateGalleryItems = () => {
|
||||
allGalleryItems.value = allTasks.value.flatMap((task: TaskItemImpl) => {
|
||||
const previewOutput = task.previewOutput
|
||||
return previewOutput ? [previewOutput] : []
|
||||
})
|
||||
}
|
||||
|
||||
const toggleExpanded = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
}
|
||||
|
||||
const removeTask = async (task: TaskItemImpl) => {
|
||||
if (task.isRunning) {
|
||||
await api.interrupt(task.promptId)
|
||||
}
|
||||
await queueStore.delete(task)
|
||||
}
|
||||
|
||||
const removeAllTasks = async () => {
|
||||
await queueStore.clear()
|
||||
}
|
||||
|
||||
const confirmRemoveAll = (event: Event) => {
|
||||
confirm.require({
|
||||
target: event.currentTarget as HTMLElement,
|
||||
message: 'Do you want to delete all tasks?',
|
||||
icon: 'pi pi-info-circle',
|
||||
rejectProps: {
|
||||
label: 'Cancel',
|
||||
severity: 'secondary',
|
||||
outlined: true
|
||||
},
|
||||
acceptProps: {
|
||||
label: 'Delete',
|
||||
severity: 'danger'
|
||||
},
|
||||
accept: async () => {
|
||||
await removeAllTasks()
|
||||
toast.add({
|
||||
severity: 'info',
|
||||
summary: 'Confirmed',
|
||||
detail: 'Tasks deleted',
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const menu = ref<InstanceType<typeof ContextMenu> | null>(null)
|
||||
const menuTargetTask = ref<TaskItemImpl | null>(null)
|
||||
const menuTargetNode = ref<ComfyNode | null>(null)
|
||||
const menuItems = computed<MenuItem[]>(() => {
|
||||
const items: MenuItem[] = [
|
||||
{
|
||||
label: t('g.delete'),
|
||||
icon: 'pi pi-trash',
|
||||
command: () => menuTargetTask.value && removeTask(menuTargetTask.value),
|
||||
disabled: isExpanded.value || isInFolderView.value
|
||||
},
|
||||
{
|
||||
label: t('g.loadWorkflow'),
|
||||
icon: 'pi pi-file-export',
|
||||
command: () => menuTargetTask.value?.loadWorkflow(app),
|
||||
disabled: isCloud
|
||||
? !menuTargetTask.value?.isHistory
|
||||
: !menuTargetTask.value?.workflow
|
||||
},
|
||||
{
|
||||
label: t('g.goToNode'),
|
||||
icon: 'pi pi-arrow-circle-right',
|
||||
command: () => {
|
||||
if (!menuTargetNode.value) return
|
||||
useLitegraphService().goToNode(menuTargetNode.value.id)
|
||||
},
|
||||
visible: !!menuTargetNode.value
|
||||
}
|
||||
]
|
||||
|
||||
if (menuTargetTask.value?.previewOutput?.mediaType === 'images') {
|
||||
items.push({
|
||||
label: t('g.setAsBackground'),
|
||||
icon: 'pi pi-image',
|
||||
command: () => {
|
||||
const url = menuTargetTask.value?.previewOutput?.url
|
||||
if (url) {
|
||||
void settingStore.set('Comfy.Canvas.BackgroundImage', url)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
const handleContextMenu = ({
|
||||
task,
|
||||
event,
|
||||
node
|
||||
}: {
|
||||
task: TaskItemImpl
|
||||
event: Event
|
||||
node: ComfyNode | null
|
||||
}) => {
|
||||
menuTargetTask.value = task
|
||||
menuTargetNode.value = node
|
||||
menu.value?.show(event)
|
||||
}
|
||||
|
||||
const handlePreview = (task: TaskItemImpl) => {
|
||||
updateGalleryItems()
|
||||
galleryActiveIndex.value = allGalleryItems.value.findIndex(
|
||||
(item) => item.url === task.previewOutput?.url
|
||||
)
|
||||
}
|
||||
|
||||
const enterFolderView = (task: TaskItemImpl) => {
|
||||
folderTask.value = task
|
||||
}
|
||||
|
||||
const exitFolderView = () => {
|
||||
folderTask.value = null
|
||||
}
|
||||
|
||||
const toggleImageFit = async () => {
|
||||
await settingStore.set(
|
||||
IMAGE_FIT,
|
||||
imageFit.value === 'cover' ? 'contain' : 'cover'
|
||||
)
|
||||
}
|
||||
|
||||
watch(allTasks, () => {
|
||||
const isGalleryOpen = galleryActiveIndex.value !== -1
|
||||
if (!isGalleryOpen) return
|
||||
|
||||
const prevLength = allGalleryItems.value.length
|
||||
updateGalleryItems()
|
||||
const lengthChange = allGalleryItems.value.length - prevLength
|
||||
if (!lengthChange) return
|
||||
|
||||
const newIndex = galleryActiveIndex.value + lengthChange
|
||||
galleryActiveIndex.value = Math.max(0, newIndex)
|
||||
})
|
||||
</script>
|
||||
@@ -1,79 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
ref="resultContainer"
|
||||
class="result-container"
|
||||
@click="handlePreviewClick"
|
||||
>
|
||||
<ComfyImage
|
||||
v-if="result.isImage"
|
||||
:src="result.url"
|
||||
class="task-output-image"
|
||||
:contain="imageFit === 'contain'"
|
||||
:alt="result.filename"
|
||||
/>
|
||||
<ResultVideo v-else-if="result.isVideo" :result="result" />
|
||||
<ResultAudio v-else-if="result.isAudio" :result="result" />
|
||||
<div v-else class="task-result-preview">
|
||||
<i class="pi pi-file" />
|
||||
<span>{{ result.mediaType }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import ComfyImage from '@/components/common/ComfyImage.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { ResultItemImpl } from '@/stores/queueStore'
|
||||
|
||||
import ResultAudio from './ResultAudio.vue'
|
||||
import ResultVideo from './ResultVideo.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
result: ResultItemImpl
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'preview', result: ResultItemImpl): void
|
||||
}>()
|
||||
|
||||
const resultContainer = ref<HTMLElement | null>(null)
|
||||
const settingStore = useSettingStore()
|
||||
const imageFit = computed<string>(() =>
|
||||
settingStore.get('Comfy.Queue.ImageFit')
|
||||
)
|
||||
|
||||
const handlePreviewClick = () => {
|
||||
if (props.result.supportsPreview) {
|
||||
emit('preview', props.result)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.result.mediaType === 'images') {
|
||||
resultContainer.value?.querySelectorAll('img').forEach((img) => {
|
||||
img.draggable = true
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.result-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.result-container:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
</style>
|
||||
@@ -1,271 +0,0 @@
|
||||
<template>
|
||||
<div class="task-item" @contextmenu="handleContextMenu">
|
||||
<div class="task-result-preview">
|
||||
<template
|
||||
v-if="
|
||||
task.displayStatus === TaskItemDisplayStatus.Completed ||
|
||||
cancelledWithResults
|
||||
"
|
||||
>
|
||||
<ResultItem
|
||||
v-if="flatOutputs.length && coverResult"
|
||||
:result="coverResult"
|
||||
@preview="handlePreview"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="task.displayStatus === TaskItemDisplayStatus.Running">
|
||||
<i v-if="!progressPreviewBlobUrl" class="pi pi-spin pi-spinner" />
|
||||
<img
|
||||
v-else
|
||||
:src="progressPreviewBlobUrl"
|
||||
class="progress-preview-img"
|
||||
/>
|
||||
</template>
|
||||
<span v-else-if="task.displayStatus === TaskItemDisplayStatus.Pending"
|
||||
>...</span
|
||||
>
|
||||
<i
|
||||
v-else-if="cancelledWithoutResults"
|
||||
class="pi pi-exclamation-triangle"
|
||||
/>
|
||||
<i
|
||||
v-else-if="task.displayStatus === TaskItemDisplayStatus.Failed"
|
||||
class="pi pi-exclamation-circle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="task-item-details">
|
||||
<div class="tag-wrapper status-tag-group">
|
||||
<Tag v-if="isFlatTask && task.isHistory" class="node-name-tag">
|
||||
<Button
|
||||
class="task-node-link"
|
||||
:label="`${node?.type} (#${node?.id})`"
|
||||
link
|
||||
size="small"
|
||||
@click="
|
||||
() => {
|
||||
if (!node) return
|
||||
litegraphService.goToNode(node.id)
|
||||
}
|
||||
"
|
||||
/>
|
||||
</Tag>
|
||||
<Tag :severity="taskTagSeverity(task.displayStatus)">
|
||||
<span v-html="taskStatusText(task.displayStatus)" />
|
||||
<span v-if="task.isHistory" class="task-time">
|
||||
{{ formatTime(task.executionTimeInSeconds) }}
|
||||
</span>
|
||||
<span v-if="isFlatTask" class="task-prompt-id">
|
||||
{{ task.promptId.split('-')[0] }}
|
||||
</span>
|
||||
</Tag>
|
||||
</div>
|
||||
<div class="tag-wrapper">
|
||||
<Button
|
||||
v-if="task.isHistory && flatOutputs.length > 1"
|
||||
outlined
|
||||
@click="handleOutputLengthClick"
|
||||
>
|
||||
<span style="font-weight: 700">{{ flatOutputs.length }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import type { ComfyNode } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { TaskItemDisplayStatus } from '@/stores/queueStore'
|
||||
import type { TaskItemImpl } from '@/stores/queueStore'
|
||||
|
||||
import ResultItem from './ResultItem.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
task: TaskItemImpl
|
||||
isFlatTask: boolean
|
||||
}>()
|
||||
|
||||
const litegraphService = useLitegraphService()
|
||||
|
||||
const flatOutputs = props.task.flatOutputs
|
||||
const coverResult = flatOutputs.length
|
||||
? props.task.previewOutput || flatOutputs[0]
|
||||
: null
|
||||
// Using `==` instead of `===` because NodeId can be a string or a number
|
||||
const node: ComfyNode | null =
|
||||
flatOutputs.length && props.task.workflow
|
||||
? (props.task.workflow.nodes.find(
|
||||
(n: ComfyNode) => n.id == coverResult?.nodeId
|
||||
) ?? null)
|
||||
: null
|
||||
const progressPreviewBlobUrl = ref('')
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
e: 'contextmenu',
|
||||
value: { task: TaskItemImpl; event: MouseEvent; node: ComfyNode | null }
|
||||
): void
|
||||
(e: 'preview', value: TaskItemImpl): void
|
||||
(e: 'task-output-length-clicked', value: TaskItemImpl): void
|
||||
}>()
|
||||
|
||||
onMounted(() => {
|
||||
api.addEventListener('b_preview', onProgressPreviewReceived)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (progressPreviewBlobUrl.value) {
|
||||
URL.revokeObjectURL(progressPreviewBlobUrl.value)
|
||||
}
|
||||
api.removeEventListener('b_preview', onProgressPreviewReceived)
|
||||
})
|
||||
|
||||
const handleContextMenu = (e: MouseEvent) => {
|
||||
emit('contextmenu', { task: props.task, event: e, node })
|
||||
}
|
||||
|
||||
const handlePreview = () => {
|
||||
emit('preview', props.task)
|
||||
}
|
||||
|
||||
const handleOutputLengthClick = () => {
|
||||
emit('task-output-length-clicked', props.task)
|
||||
}
|
||||
|
||||
const taskTagSeverity = (status: TaskItemDisplayStatus) => {
|
||||
switch (status) {
|
||||
case TaskItemDisplayStatus.Pending:
|
||||
return 'secondary'
|
||||
case TaskItemDisplayStatus.Running:
|
||||
return 'info'
|
||||
case TaskItemDisplayStatus.Completed:
|
||||
return 'success'
|
||||
case TaskItemDisplayStatus.Failed:
|
||||
return 'danger'
|
||||
case TaskItemDisplayStatus.Cancelled:
|
||||
return 'warn'
|
||||
}
|
||||
}
|
||||
|
||||
const taskStatusText = (status: TaskItemDisplayStatus) => {
|
||||
switch (status) {
|
||||
case TaskItemDisplayStatus.Pending:
|
||||
return 'Pending'
|
||||
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>'
|
||||
case TaskItemDisplayStatus.Failed:
|
||||
return 'Failed'
|
||||
case TaskItemDisplayStatus.Cancelled:
|
||||
return 'Cancelled'
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (time?: number) => {
|
||||
if (time === undefined) {
|
||||
return ''
|
||||
}
|
||||
return `${time.toFixed(2)}s`
|
||||
}
|
||||
|
||||
const onProgressPreviewReceived = async ({ detail }: CustomEvent) => {
|
||||
if (props.task.displayStatus === TaskItemDisplayStatus.Running) {
|
||||
if (progressPreviewBlobUrl.value) {
|
||||
URL.revokeObjectURL(progressPreviewBlobUrl.value)
|
||||
}
|
||||
progressPreviewBlobUrl.value = URL.createObjectURL(detail)
|
||||
}
|
||||
}
|
||||
|
||||
const cancelledWithResults = computed(() => {
|
||||
return (
|
||||
props.task.displayStatus === TaskItemDisplayStatus.Cancelled &&
|
||||
flatOutputs.length
|
||||
)
|
||||
})
|
||||
|
||||
const cancelledWithoutResults = computed(() => {
|
||||
return (
|
||||
props.task.displayStatus === TaskItemDisplayStatus.Cancelled &&
|
||||
flatOutputs.length === 0
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-result-preview {
|
||||
aspect-ratio: 1 / 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.task-result-preview i,
|
||||
.task-result-preview span {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.task-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.task-item-details {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
padding: 0.6rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
pointer-events: none; /* Allow clicks to pass through this div */
|
||||
}
|
||||
|
||||
/* Make individual controls clickable again by restoring pointer events */
|
||||
.task-item-details .tag-wrapper,
|
||||
.task-item-details button {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.task-node-link {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
/* In dark mode, transparent background color for tags is not ideal for tags that
|
||||
are floating on top of images. */
|
||||
.tag-wrapper {
|
||||
background-color: var(--p-primary-contrast-color);
|
||||
border-radius: 6px;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.node-name-tag {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.status-tag-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.progress-preview-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
</style>
|
||||
@@ -181,7 +181,6 @@ Composables for sidebar functionality:
|
||||
|------------|-------------|
|
||||
| `useModelLibrarySidebarTab` | Manages the model library sidebar tab |
|
||||
| `useNodeLibrarySidebarTab` | Manages the node library sidebar tab |
|
||||
| `useQueueSidebarTab` | Manages the queue sidebar tab |
|
||||
| `useWorkflowsSidebarTab` | Manages the workflows sidebar tab |
|
||||
|
||||
### Tree
|
||||
|
||||
@@ -13,7 +13,6 @@ import type {
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
import type { NodeId } from '@/renderer/core/layout/types'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { isDOMWidget } from '@/scripts/domWidget'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
@@ -47,7 +46,7 @@ export interface SafeWidgetData {
|
||||
}
|
||||
|
||||
export interface VueNodeData {
|
||||
id: NodeId
|
||||
id: string
|
||||
title: string
|
||||
type: string
|
||||
mode: number
|
||||
@@ -79,10 +78,64 @@ export interface GraphNodeManager {
|
||||
cleanup(): void
|
||||
}
|
||||
|
||||
export function safeWidgetMapper(
|
||||
node: LGraphNode,
|
||||
slotMetadata: Map<string, WidgetSlotMetadata>
|
||||
): (widget: IBaseWidget) => SafeWidgetData {
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
return function (widget) {
|
||||
try {
|
||||
// TODO: Use widget.getReactiveData() once TypeScript types are updated
|
||||
let value = widget.value
|
||||
|
||||
// For combo widgets, if value is undefined, use the first option as default
|
||||
if (
|
||||
value === undefined &&
|
||||
widget.type === 'combo' &&
|
||||
widget.options?.values &&
|
||||
Array.isArray(widget.options.values) &&
|
||||
widget.options.values.length > 0
|
||||
) {
|
||||
value = widget.options.values[0]
|
||||
}
|
||||
const spec = nodeDefStore.getInputSpecForWidget(node, widget.name)
|
||||
const slotInfo = slotMetadata.get(widget.name)
|
||||
|
||||
return {
|
||||
name: widget.name,
|
||||
type: widget.type,
|
||||
value: value,
|
||||
label: widget.label,
|
||||
options: widget.options ? { ...widget.options } : undefined,
|
||||
callback: widget.callback,
|
||||
spec,
|
||||
slotMetadata: slotInfo,
|
||||
isDOMWidget: isDOMWidget(widget)
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
name: widget.name || 'unknown',
|
||||
type: widget.type || 'text',
|
||||
value: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isValidWidgetValue(value: unknown): value is WidgetValue {
|
||||
return (
|
||||
value === null ||
|
||||
value === undefined ||
|
||||
typeof value === 'string' ||
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'boolean' ||
|
||||
typeof value === 'object'
|
||||
)
|
||||
}
|
||||
|
||||
export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
// Get layout mutations composable
|
||||
const { createNode, deleteNode, setSource } = useLayoutMutations()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
// Safe reactive data extracted from LiteGraph nodes
|
||||
const vueNodeData = reactive(new Map<string, VueNodeData>())
|
||||
|
||||
@@ -148,45 +201,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
linked: input.link != null
|
||||
})
|
||||
})
|
||||
return (
|
||||
node.widgets?.map((widget) => {
|
||||
try {
|
||||
// TODO: Use widget.getReactiveData() once TypeScript types are updated
|
||||
let value = widget.value
|
||||
|
||||
// For combo widgets, if value is undefined, use the first option as default
|
||||
if (
|
||||
value === undefined &&
|
||||
widget.type === 'combo' &&
|
||||
widget.options?.values &&
|
||||
Array.isArray(widget.options.values) &&
|
||||
widget.options.values.length > 0
|
||||
) {
|
||||
value = widget.options.values[0]
|
||||
}
|
||||
const spec = nodeDefStore.getInputSpecForWidget(node, widget.name)
|
||||
const slotInfo = slotMetadata.get(widget.name)
|
||||
|
||||
return {
|
||||
name: widget.name,
|
||||
type: widget.type,
|
||||
value: value,
|
||||
label: widget.label,
|
||||
options: widget.options ? { ...widget.options } : undefined,
|
||||
callback: widget.callback,
|
||||
spec,
|
||||
slotMetadata: slotInfo,
|
||||
isDOMWidget: isDOMWidget(widget)
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
name: widget.name || 'unknown',
|
||||
type: widget.type || 'text',
|
||||
value: undefined
|
||||
}
|
||||
}
|
||||
}) ?? []
|
||||
)
|
||||
return node.widgets?.map(safeWidgetMapper(node, slotMetadata)) ?? []
|
||||
})
|
||||
|
||||
const nodeType =
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import TopBarHeader from '@/components/maskeditor/dialog/TopBarHeader.vue'
|
||||
import MaskEditorContent from '@/components/maskeditor/MaskEditorContent.vue'
|
||||
|
||||
export function useMaskEditor() {
|
||||
const openMaskEditor = (node: LGraphNode) => {
|
||||
if (!node) {
|
||||
console.error('[MaskEditor] No node provided')
|
||||
return
|
||||
}
|
||||
|
||||
if (!node.imgs?.length && node.previewMediaType !== 'image') {
|
||||
console.error('[MaskEditor] Node has no images')
|
||||
return
|
||||
}
|
||||
|
||||
useDialogStore().showDialog({
|
||||
key: 'global-mask-editor',
|
||||
headerComponent: TopBarHeader,
|
||||
component: MaskEditorContent,
|
||||
props: {
|
||||
node
|
||||
},
|
||||
dialogComponentProps: {
|
||||
style: 'width: 90vw; height: 90vh;',
|
||||
modal: true,
|
||||
maximizable: true,
|
||||
closable: true,
|
||||
pt: {
|
||||
root: {
|
||||
class: 'mask-editor-dialog flex flex-col'
|
||||
},
|
||||
content: {
|
||||
class: 'flex flex-col min-h-0 flex-1 !p-0'
|
||||
},
|
||||
header: {
|
||||
class: '!p-2'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
openMaskEditor
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
import QueueSidebarTab from '@/components/sidebar/tabs/QueueSidebarTab.vue'
|
||||
import { useQueuePendingTaskCountStore } from '@/stores/queueStore'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
export const useQueueSidebarTab = (): SidebarTabExtension => {
|
||||
const queuePendingTaskCountStore = useQueuePendingTaskCountStore()
|
||||
return {
|
||||
id: 'queue',
|
||||
icon: 'pi pi-history',
|
||||
iconBadge: () => {
|
||||
const value = queuePendingTaskCountStore.count.toString()
|
||||
return value === '0' ? null : value
|
||||
},
|
||||
title: 'sideToolbar.queue',
|
||||
tooltip: 'sideToolbar.queue',
|
||||
label: 'sideToolbar.labels.queue',
|
||||
component: markRaw(QueueSidebarTab),
|
||||
type: 'vue'
|
||||
}
|
||||
}
|
||||
@@ -1219,6 +1219,12 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
await settingStore.set('Comfy.Assets.UseAssetAPI', !current)
|
||||
await useWorkflowService().reloadCurrentWorkflow() // ensure changes take effect immediately
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.ToggleLinear',
|
||||
icon: 'pi pi-database',
|
||||
label: 'toggle linear mode',
|
||||
function: () => (canvasStore.linearMode = !canvasStore.linearMode)
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
344
src/composables/useLivePreview.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
IBaseWidget,
|
||||
TWidgetValue
|
||||
} from '@/lib/litegraph/src/types/widgets'
|
||||
|
||||
interface PropagationOptions {
|
||||
/**
|
||||
* Find output by name instead of index
|
||||
*/
|
||||
outputName?: string
|
||||
|
||||
/**
|
||||
* Explicitly specify output index (default: 0)
|
||||
*/
|
||||
outputIndex?: number
|
||||
|
||||
/**
|
||||
* Whether to call node.setOutputData (default: false)
|
||||
*/
|
||||
setOutputData?: boolean
|
||||
|
||||
/**
|
||||
* Whether to update target widget values (default: true)
|
||||
*/
|
||||
updateWidget?: boolean
|
||||
|
||||
/**
|
||||
* Whether to call widget.callback after updating (default: false)
|
||||
*/
|
||||
callWidgetCallback?: boolean
|
||||
|
||||
/**
|
||||
* Whether to call targetNode.onExecuted (default: false)
|
||||
*/
|
||||
callOnExecuted?: boolean
|
||||
|
||||
/**
|
||||
* Custom function to build the message for onExecuted
|
||||
*/
|
||||
messageBuilder?: (
|
||||
targetNode: LGraphNode,
|
||||
value: TWidgetValue,
|
||||
link: any
|
||||
) => any
|
||||
|
||||
/**
|
||||
* Custom handlers for specific node types
|
||||
* Return true if handled, false to continue with default behavior
|
||||
*/
|
||||
customHandlers?: Map<
|
||||
string,
|
||||
(node: LGraphNode, value: TWidgetValue, link: any) => boolean
|
||||
>
|
||||
|
||||
/**
|
||||
* Enable reentry protection (default: true)
|
||||
*/
|
||||
preventReentry?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculator function type for live preview nodes
|
||||
* Takes input values and returns the computed output value
|
||||
*/
|
||||
type LivePreviewCalculator = (inputValues: any[]) => TWidgetValue
|
||||
|
||||
/**
|
||||
* Configuration for setting up a live preview node
|
||||
*/
|
||||
interface LivePreviewNodeConfig {
|
||||
/**
|
||||
* The calculator function that computes output from inputs
|
||||
*/
|
||||
calculator: LivePreviewCalculator
|
||||
|
||||
/**
|
||||
* Optional output index (default: 0)
|
||||
*/
|
||||
outputIndex?: number
|
||||
|
||||
/**
|
||||
* Optional propagation options to use when propagating the result
|
||||
*/
|
||||
propagationOptions?: Omit<PropagationOptions, 'outputIndex' | 'setOutputData'>
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for managing live preview functionality in ComfyUI nodes
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In a node extension:
|
||||
* const { setupLivePreviewNode, propagateLivePreview } = useLivePreview()
|
||||
*
|
||||
* // For computation nodes:
|
||||
* setupLivePreviewNode(node, {
|
||||
* calculator: (inputs) => {
|
||||
* const [a, b] = inputs
|
||||
* return a + b
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* // For simple propagation:
|
||||
* propagateLivePreview(node, value, {
|
||||
* updateWidget: true,
|
||||
* callOnExecuted: true
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
const propagationFlags = new WeakMap<LGraphNode, Set<string>>()
|
||||
const nodeCalculators = new WeakMap<LGraphNode, LivePreviewNodeConfig>()
|
||||
|
||||
export function useLivePreview() {
|
||||
function getPropagationKey(outputIndex: number): string {
|
||||
return `propagating_${outputIndex}`
|
||||
}
|
||||
|
||||
function isNodePropagating(node: LGraphNode, outputIndex: number): boolean {
|
||||
const flags = propagationFlags.get(node)
|
||||
return flags?.has(getPropagationKey(outputIndex)) ?? false
|
||||
}
|
||||
|
||||
function setNodePropagating(
|
||||
node: LGraphNode,
|
||||
outputIndex: number,
|
||||
value: boolean
|
||||
): void {
|
||||
if (!propagationFlags.has(node)) {
|
||||
propagationFlags.set(node, new Set())
|
||||
}
|
||||
const flags = propagationFlags.get(node)!
|
||||
const key = getPropagationKey(outputIndex)
|
||||
|
||||
if (value) {
|
||||
flags.add(key)
|
||||
} else {
|
||||
flags.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
function collectNodeInputValues(node: LGraphNode): any[] {
|
||||
const inputValues: any[] = []
|
||||
const graph = node.graph as LGraph
|
||||
|
||||
if (!graph || !node.inputs) {
|
||||
return inputValues
|
||||
}
|
||||
|
||||
for (const input of node.inputs) {
|
||||
if (input.link != null) {
|
||||
const link = graph.links[input.link]
|
||||
if (link) {
|
||||
const sourceNode = graph.getNodeById(link.origin_id)
|
||||
if (sourceNode && sourceNode.getOutputData) {
|
||||
const outputData = sourceNode.getOutputData(link.origin_slot)
|
||||
inputValues.push(outputData)
|
||||
} else {
|
||||
inputValues.push(undefined)
|
||||
}
|
||||
} else {
|
||||
inputValues.push(undefined)
|
||||
}
|
||||
} else if (input.widget) {
|
||||
const widget = node.widgets?.find((w) => w.name === input.widget?.name)
|
||||
inputValues.push(widget?.value)
|
||||
} else {
|
||||
inputValues.push(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
return inputValues
|
||||
}
|
||||
|
||||
function triggerNodeRecalculation(node: LGraphNode): void {
|
||||
const config = nodeCalculators.get(node)
|
||||
if (!config) {
|
||||
return
|
||||
}
|
||||
|
||||
const inputValues = collectNodeInputValues(node)
|
||||
|
||||
const hasValidInputs = inputValues.some((v) => v !== undefined)
|
||||
if (!hasValidInputs) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = config.calculator(inputValues)
|
||||
if (result !== undefined) {
|
||||
propagateLivePreview(node, result, {
|
||||
outputIndex: config.outputIndex ?? 0,
|
||||
setOutputData: true,
|
||||
...config.propagationOptions
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error calculating live preview for node ${node.type}:`,
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function propagateLivePreview(
|
||||
sourceNode: LGraphNode,
|
||||
value: TWidgetValue,
|
||||
options: PropagationOptions = {}
|
||||
): void {
|
||||
const {
|
||||
outputName,
|
||||
outputIndex: explicitOutputIndex,
|
||||
setOutputData = false,
|
||||
updateWidget = true,
|
||||
callWidgetCallback = false,
|
||||
callOnExecuted = false,
|
||||
messageBuilder,
|
||||
customHandlers,
|
||||
preventReentry = true
|
||||
} = options
|
||||
|
||||
let outputIndex = explicitOutputIndex ?? 0
|
||||
|
||||
if (outputName && sourceNode.outputs) {
|
||||
const foundIndex = sourceNode.outputs.findIndex(
|
||||
(output) => output.name === outputName
|
||||
)
|
||||
if (foundIndex >= 0) {
|
||||
outputIndex = foundIndex
|
||||
}
|
||||
}
|
||||
|
||||
if (preventReentry && isNodePropagating(sourceNode, outputIndex)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (preventReentry) {
|
||||
setNodePropagating(sourceNode, outputIndex, true)
|
||||
}
|
||||
|
||||
try {
|
||||
if (setOutputData && sourceNode.setOutputData && value !== undefined) {
|
||||
sourceNode.setOutputData(outputIndex, value as any)
|
||||
}
|
||||
|
||||
const output = sourceNode.outputs?.[outputIndex]
|
||||
if (!output || !output.links || output.links.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const graph = sourceNode.graph as LGraph
|
||||
if (!graph) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const linkId of output.links) {
|
||||
const link = graph.links[linkId]
|
||||
if (!link) {
|
||||
continue
|
||||
}
|
||||
|
||||
const targetNode = graph.getNodeById(link.target_id)
|
||||
if (!targetNode) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (customHandlers?.has(targetNode.type)) {
|
||||
const handler = customHandlers.get(targetNode.type)!
|
||||
const handled = handler(targetNode, value, link)
|
||||
if (handled) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (updateWidget) {
|
||||
const targetInput = targetNode.inputs?.[link.target_slot]
|
||||
if (targetInput?.widget) {
|
||||
const targetWidget = targetNode.widgets?.find(
|
||||
(w: IBaseWidget) => w.name === targetInput.widget?.name
|
||||
)
|
||||
|
||||
if (targetWidget) {
|
||||
targetWidget.value = value
|
||||
|
||||
if (callWidgetCallback && targetWidget.callback) {
|
||||
targetWidget.callback(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasCalculator = nodeCalculators.has(targetNode)
|
||||
|
||||
if (hasCalculator) {
|
||||
triggerNodeRecalculation(targetNode)
|
||||
continue
|
||||
}
|
||||
|
||||
if (callOnExecuted && targetNode.onExecuted) {
|
||||
const message = messageBuilder
|
||||
? messageBuilder(targetNode, value, link)
|
||||
: { text: [value] }
|
||||
|
||||
targetNode.onExecuted(message)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (preventReentry) {
|
||||
setNodePropagating(sourceNode, outputIndex, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupLivePreviewNode(
|
||||
node: LGraphNode,
|
||||
config: LivePreviewNodeConfig
|
||||
): void {
|
||||
nodeCalculators.set(node, config)
|
||||
|
||||
const originalOnExecuted = node.onExecuted
|
||||
node.onExecuted = function (message: any) {
|
||||
if (originalOnExecuted) {
|
||||
originalOnExecuted.call(this, message)
|
||||
}
|
||||
|
||||
if (message.text && Array.isArray(message.text)) {
|
||||
const result = config.calculator(message.text)
|
||||
if (result !== undefined) {
|
||||
propagateLivePreview(this, result, {
|
||||
outputIndex: config.outputIndex ?? 0,
|
||||
setOutputData: true,
|
||||
...config.propagationOptions
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
propagateLivePreview,
|
||||
setupLivePreviewNode
|
||||
}
|
||||
}
|
||||
@@ -30,12 +30,6 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
},
|
||||
commandId: 'Comfy.RefreshNodeDefinitions'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'q'
|
||||
},
|
||||
commandId: 'Workspace.ToggleSidebarTab.queue'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'w'
|
||||
|
||||
114
src/core/graph/widgets/dynamicWidgets.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration'
|
||||
import type { ComboInputSpec, InputSpec } from '@/schemas/nodeDefSchema'
|
||||
import { zDynamicComboInputSpec } from '@/schemas/nodeDefSchema'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
|
||||
function dynamicComboWidget(
|
||||
node: LGraphNode,
|
||||
inputName: string,
|
||||
untypedInputData: InputSpec,
|
||||
appArg: ComfyApp,
|
||||
widgetName?: string
|
||||
) {
|
||||
const { addNodeInput } = useLitegraphService()
|
||||
const parseResult = zDynamicComboInputSpec.safeParse(untypedInputData)
|
||||
if (!parseResult.success) throw new Error('invalid DynamicCombo spec')
|
||||
const inputData = parseResult.data
|
||||
const options = Object.fromEntries(
|
||||
inputData[1].options.map(({ key, inputs }) => [key, inputs])
|
||||
)
|
||||
const subSpec: ComboInputSpec = [Object.keys(options), {}]
|
||||
const { widget, minWidth, minHeight } = app.widgets['COMBO'](
|
||||
node,
|
||||
inputName,
|
||||
subSpec,
|
||||
appArg,
|
||||
widgetName
|
||||
)
|
||||
let currentDynamicNames: string[] = []
|
||||
const updateWidgets = (value?: string) => {
|
||||
if (!node.widgets) throw new Error('Not Reachable')
|
||||
const newSpec = value ? options[value] : undefined
|
||||
//TODO: Calculate intersection for widgets that persist across options
|
||||
//This would potentially allow links to be retained
|
||||
for (const name of currentDynamicNames) {
|
||||
const inputIndex = node.inputs.findIndex((input) => input.name === name)
|
||||
if (inputIndex !== -1) node.removeInput(inputIndex)
|
||||
const widgetIndex = node.widgets.findIndex(
|
||||
(widget) => widget.name === name
|
||||
)
|
||||
if (widgetIndex === -1) continue
|
||||
node.widgets[widgetIndex].value = undefined
|
||||
node.widgets.splice(widgetIndex, 1)
|
||||
}
|
||||
currentDynamicNames = []
|
||||
if (!newSpec) return
|
||||
|
||||
const insertionPoint = node.widgets.findIndex((w) => w === widget) + 1
|
||||
const startingLength = node.widgets.length
|
||||
const inputInsertionPoint =
|
||||
node.inputs.findIndex((i) => i.name === widget.name) + 1
|
||||
const startingInputLength = node.inputs.length
|
||||
if (insertionPoint === 0)
|
||||
throw new Error("Dynamic widget doesn't exist on node")
|
||||
const inputTypes: [Record<string, InputSpec> | undefined, boolean][] = [
|
||||
[newSpec.required, false],
|
||||
[newSpec.optional, true]
|
||||
]
|
||||
for (const [inputType, isOptional] of inputTypes)
|
||||
for (const name in inputType ?? {}) {
|
||||
addNodeInput(
|
||||
node,
|
||||
transformInputSpecV1ToV2(inputType![name], {
|
||||
name,
|
||||
isOptional
|
||||
})
|
||||
)
|
||||
currentDynamicNames.push(name)
|
||||
}
|
||||
|
||||
const addedWidgets = node.widgets.splice(startingLength)
|
||||
node.widgets.splice(insertionPoint, 0, ...addedWidgets)
|
||||
if (inputInsertionPoint === 0) {
|
||||
if (
|
||||
addedWidgets.length === 0 &&
|
||||
node.inputs.length !== startingInputLength
|
||||
)
|
||||
//input is inputOnly, but lacks an insertion point
|
||||
throw new Error('Failed to find input socket for ' + widget.name)
|
||||
return
|
||||
}
|
||||
const addedInputs = node
|
||||
.spliceInputs(startingInputLength)
|
||||
.map((addedInput) => {
|
||||
const existingInput = node.inputs.findIndex(
|
||||
(existingInput) => addedInput.name === existingInput.name
|
||||
)
|
||||
return existingInput === -1
|
||||
? addedInput
|
||||
: node.spliceInputs(existingInput, 1)[0]
|
||||
})
|
||||
//assume existing inputs are in correct order
|
||||
node.spliceInputs(inputInsertionPoint, 0, ...addedInputs)
|
||||
node.size[1] = node.computeSize([...node.size])[1]
|
||||
}
|
||||
//A little hacky, but onConfigure won't work.
|
||||
//It fires too late and is overly disruptive
|
||||
let widgetValue = widget.value
|
||||
Object.defineProperty(widget, 'value', {
|
||||
get() {
|
||||
return widgetValue
|
||||
},
|
||||
set(value) {
|
||||
widgetValue = value
|
||||
updateWidgets(value)
|
||||
}
|
||||
})
|
||||
widget.value = widgetValue
|
||||
return { widget, minWidth, minHeight }
|
||||
}
|
||||
|
||||
export const dynamicWidgets = { COMFY_DYNAMICCOMBO_V3: dynamicComboWidget }
|
||||
@@ -14,6 +14,7 @@ import './matchType'
|
||||
import './nodeTemplates'
|
||||
import './noteNode'
|
||||
import './previewAny'
|
||||
import './stringOperations'
|
||||
import './rerouteNode'
|
||||
import './saveImageExtraOutput'
|
||||
import './saveMesh'
|
||||
|
||||
@@ -5,9 +5,10 @@ import { app } from '@/scripts/app'
|
||||
import { ComfyApp } from '@/scripts/app'
|
||||
import { useMaskEditorStore } from '@/stores/maskEditorStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import MaskEditorContent from '@/components/maskeditor/MaskEditorContent.vue'
|
||||
import TopBarHeader from '@/components/maskeditor/dialog/TopBarHeader.vue'
|
||||
import { MaskEditorDialogOld } from './maskEditorOld'
|
||||
import { ClipspaceDialog } from './clipspace'
|
||||
import { useMaskEditor } from '@/composables/maskeditor/useMaskEditor'
|
||||
|
||||
function openMaskEditor(node: LGraphNode): void {
|
||||
if (!node) {
|
||||
@@ -25,7 +26,32 @@ function openMaskEditor(node: LGraphNode): void {
|
||||
)
|
||||
|
||||
if (useNewEditor) {
|
||||
useMaskEditor().openMaskEditor(node)
|
||||
// Use new refactored editor
|
||||
useDialogStore().showDialog({
|
||||
key: 'global-mask-editor',
|
||||
headerComponent: TopBarHeader,
|
||||
component: MaskEditorContent,
|
||||
props: {
|
||||
node
|
||||
},
|
||||
dialogComponentProps: {
|
||||
style: 'width: 90vw; height: 90vh;',
|
||||
modal: true,
|
||||
maximizable: true,
|
||||
closable: true,
|
||||
pt: {
|
||||
root: {
|
||||
class: 'mask-editor-dialog flex flex-col'
|
||||
},
|
||||
content: {
|
||||
class: 'flex flex-col min-h-0 flex-1 !p-0'
|
||||
},
|
||||
header: {
|
||||
class: '!p-2'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Use old editor
|
||||
ComfyApp.copyToClipspace(node)
|
||||
|
||||
@@ -17,10 +17,10 @@ useExtensionService().registerExtension({
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
onNodeCreated ? onNodeCreated.apply(this, []) : undefined
|
||||
|
||||
const showValueWidget = ComfyWidgets['MARKDOWN'](
|
||||
const showValueWidget = ComfyWidgets['STRING'](
|
||||
this,
|
||||
'preview',
|
||||
['MARKDOWN', {}],
|
||||
['STRING', { multiline: true }],
|
||||
app
|
||||
).widget as DOMWidget<HTMLTextAreaElement, string>
|
||||
|
||||
|
||||
58
src/extensions/core/stringOperations.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { useLivePreview } from '@/composables/useLivePreview'
|
||||
|
||||
const { setupLivePreviewNode } = useLivePreview()
|
||||
|
||||
useExtensionService().registerExtension({
|
||||
name: 'Comfy.StringLength',
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (nodeData.name === 'StringLength') {
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
if (onNodeCreated) {
|
||||
onNodeCreated.call(this)
|
||||
}
|
||||
|
||||
// Set up live preview with calculator
|
||||
setupLivePreviewNode(this, {
|
||||
calculator: (inputs) => {
|
||||
const inputString = inputs[0]
|
||||
if (inputString == null) return undefined
|
||||
return String(inputString).length
|
||||
},
|
||||
propagationOptions: {
|
||||
updateWidget: true,
|
||||
callOnExecuted: true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
useExtensionService().registerExtension({
|
||||
name: 'Comfy.StringConcatenate',
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (nodeData.name === 'StringConcatenate') {
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
if (onNodeCreated) {
|
||||
onNodeCreated.call(this)
|
||||
}
|
||||
|
||||
// Set up live preview with calculator
|
||||
setupLivePreviewNode(this, {
|
||||
calculator: (inputs) => {
|
||||
const [string_a, string_b, delimiter] = inputs
|
||||
if (string_a == null && string_b == null) return undefined
|
||||
return [string_a ?? '', string_b ?? ''].join(delimiter || '')
|
||||
},
|
||||
propagationOptions: {
|
||||
updateWidget: true,
|
||||
callOnExecuted: true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -79,7 +79,7 @@ export type {
|
||||
LGraphTriggerParam
|
||||
} from './types/graphTriggers'
|
||||
|
||||
export type RendererType = 'LG' | 'Vue'
|
||||
export type rendererType = 'LG' | 'Vue'
|
||||
|
||||
export interface LGraphState {
|
||||
lastGroupId: number
|
||||
@@ -106,7 +106,7 @@ export interface LGraphExtra extends Dictionary<unknown> {
|
||||
reroutes?: SerialisableReroute[]
|
||||
linkExtensions?: { id: number; parentId: number | undefined }[]
|
||||
ds?: DragAndScaleState
|
||||
workflowRendererVersion?: RendererType
|
||||
workflowRendererVersion?: rendererType
|
||||
}
|
||||
|
||||
export interface BaseLGraph {
|
||||
|
||||
@@ -1771,19 +1771,18 @@ export class LGraphCanvas
|
||||
}
|
||||
|
||||
static onMenuNodeClone(
|
||||
_value: IContextMenuValue,
|
||||
_options: IContextMenuOptions,
|
||||
_e: MouseEvent,
|
||||
_menu: ContextMenu,
|
||||
// @ts-expect-error - unused parameter
|
||||
value: IContextMenuValue,
|
||||
// @ts-expect-error - unused parameter
|
||||
options: IContextMenuOptions,
|
||||
// @ts-expect-error - unused parameter
|
||||
e: MouseEvent,
|
||||
// @ts-expect-error - unused parameter
|
||||
menu: ContextMenu,
|
||||
node: LGraphNode
|
||||
): void {
|
||||
const canvas = LGraphCanvas.active_canvas
|
||||
const nodes = canvas.selectedItems.size ? [...canvas.selectedItems] : [node]
|
||||
if (nodes.length) LGraphCanvas.cloneNodes(nodes)
|
||||
}
|
||||
|
||||
static cloneNodes(nodes: Positionable[]) {
|
||||
const canvas = LGraphCanvas.active_canvas
|
||||
const nodes = canvas.selectedItems.size ? canvas.selectedItems : [node]
|
||||
|
||||
// Find top-left-most boundary
|
||||
let offsetX = Infinity
|
||||
@@ -1793,11 +1792,11 @@ export class LGraphCanvas
|
||||
throw new TypeError(
|
||||
'Invalid node encountered on clone. `pos` was null.'
|
||||
)
|
||||
offsetX = Math.min(offsetX, item.pos[0])
|
||||
offsetY = Math.min(offsetY, item.pos[1])
|
||||
if (item.pos[0] < offsetX) offsetX = item.pos[0]
|
||||
if (item.pos[1] < offsetY) offsetY = item.pos[1]
|
||||
}
|
||||
|
||||
return canvas._deserializeItems(canvas._serializeItems(nodes), {
|
||||
canvas._deserializeItems(canvas._serializeItems(nodes), {
|
||||
position: [offsetX + 5, offsetY + 5]
|
||||
})
|
||||
}
|
||||
|
||||
@@ -835,6 +835,9 @@ export class LGraphNode
|
||||
for (const w of this.widgets) {
|
||||
if (!w) continue
|
||||
|
||||
const input = this.inputs.find((i) => i.widget?.name === w.name)
|
||||
if (input?.label) w.label = input.label
|
||||
|
||||
if (
|
||||
w.options?.property &&
|
||||
this.properties[w.options.property] != undefined
|
||||
@@ -845,15 +848,13 @@ export class LGraphNode
|
||||
}
|
||||
|
||||
if (info.widgets_values) {
|
||||
const widgetsWithValue = this.widgets.filter(
|
||||
(w) => w.serialize !== false
|
||||
const widgetsWithValue = this.widgets
|
||||
.values()
|
||||
.filter((w) => w.serialize !== false)
|
||||
.filter((_w, idx) => idx < info.widgets_values!.length)
|
||||
widgetsWithValue.forEach(
|
||||
(widget, i) => (widget.value = info.widgets_values![i])
|
||||
)
|
||||
for (let i = 0; i < info.widgets_values.length; ++i) {
|
||||
const widget = widgetsWithValue[i]
|
||||
if (widget) {
|
||||
widget.value = info.widgets_values[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -881,7 +882,7 @@ export class LGraphNode
|
||||
|
||||
// special case for when there were errors
|
||||
if (this.constructor === LGraphNode && this.last_serialization)
|
||||
return this.last_serialization
|
||||
return { ...this.last_serialization, mode: o.mode, pos: o.pos }
|
||||
|
||||
if (this.inputs)
|
||||
o.inputs = this.inputs.map((input) => inputAsSerialisable(input))
|
||||
@@ -1649,6 +1650,19 @@ export class LGraphNode
|
||||
this.onInputRemoved?.(slot, slot_info[0])
|
||||
this.setDirtyCanvas(true, true)
|
||||
}
|
||||
spliceInputs(
|
||||
startIndex: number,
|
||||
deleteCount = -1,
|
||||
...toAdd: INodeInputSlot[]
|
||||
): INodeInputSlot[] {
|
||||
if (deleteCount < 0) return this.inputs.splice(startIndex)
|
||||
const ret = this.inputs.splice(startIndex, deleteCount, ...toAdd)
|
||||
this.inputs.slice(startIndex).forEach((input, index) => {
|
||||
const link = input.link && this.graph?.links?.get(input.link)
|
||||
if (link) link.target_slot = startIndex + index
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
* computes the minimum size of a node according to its inputs and output slots
|
||||
|
||||
@@ -1,43 +1,10 @@
|
||||
{
|
||||
"Comfy-Desktop_CheckForUpdates": {
|
||||
"label": "التحقق من التحديثات"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "فتح مجلد العقد المخصصة"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenInputsFolder": {
|
||||
"label": "فتح مجلد المدخلات"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenLogsFolder": {
|
||||
"label": "فتح مجلد السجلات"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelConfig": {
|
||||
"label": "فتح extra_model_paths.yaml"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelsFolder": {
|
||||
"label": "فتح مجلد النماذج"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenOutputsFolder": {
|
||||
"label": "فتح مجلد المخرجات"
|
||||
},
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "فتح أدوات المطور"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "دليل المستخدم لسطح المكتب"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "خروج"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "إعادة التثبيت"
|
||||
},
|
||||
"Comfy-Desktop_Restart": {
|
||||
"label": "إعادة التشغيل"
|
||||
},
|
||||
"Comfy_3DViewer_Open3DViewer": {
|
||||
"label": "فتح عارض ثلاثي الأبعاد (بيتا) للعقدة المحددة"
|
||||
},
|
||||
"Comfy_BrowseModelAssets": {
|
||||
"label": "تجريبي: تصفح أصول النماذج"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "تصفح القوالب"
|
||||
},
|
||||
@@ -125,6 +92,9 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "تحويل التحديد إلى رسم فرعي"
|
||||
},
|
||||
"Comfy_Graph_EditSubgraphWidgets": {
|
||||
"label": "تحرير عناصر واجهة الرسم البياني الفرعي"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "الخروج من الرسم البياني الفرعي"
|
||||
},
|
||||
@@ -134,6 +104,9 @@
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "تجميع العقد المحددة"
|
||||
},
|
||||
"Comfy_Graph_ToggleWidgetPromotion": {
|
||||
"label": "تبديل ترقية عنصر الواجهة المحوم فوقه"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "فك التفرع الفرعي المحدد"
|
||||
},
|
||||
@@ -239,6 +212,9 @@
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "عرض نافذة الإعدادات"
|
||||
},
|
||||
"Comfy_ToggleAssetAPI": {
|
||||
"label": "تجريبي: تمكين AssetAPI"
|
||||
},
|
||||
"Comfy_ToggleCanvasInfo": {
|
||||
"label": "أداء اللوحة"
|
||||
},
|
||||
@@ -257,6 +233,9 @@
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "تسجيل الخروج"
|
||||
},
|
||||
"Experimental_ToggleVueNodes": {
|
||||
"label": "تجريبي: تمكين عقد Vue"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "إغلاق سير العمل الحالي"
|
||||
},
|
||||
@@ -290,6 +269,10 @@
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "تبديل وضع التركيز"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "تبديل الشريط الجانبي للأصول",
|
||||
"tooltip": "الأصول"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_model-library": {
|
||||
"label": "تبديل الشريط الجانبي لمكتبة النماذج",
|
||||
"tooltip": "مكتبة النماذج"
|
||||
@@ -298,31 +281,8 @@
|
||||
"label": "تبديل الشريط الجانبي لمكتبة العقد",
|
||||
"tooltip": "مكتبة العقد"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_queue": {
|
||||
"label": "تبديل الشريط الجانبي لقائمة الانتظار",
|
||||
"tooltip": "قائمة الانتظار"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_workflows": {
|
||||
"label": "تبديل الشريط الجانبي لسير العمل",
|
||||
"tooltip": "سير العمل"
|
||||
},
|
||||
"Comfy_BrowseModelAssets": {
|
||||
"label": "تجريبي: تصفح أصول النماذج"
|
||||
},
|
||||
"Comfy_Graph_EditSubgraphWidgets": {
|
||||
"label": "تحرير عناصر واجهة الرسم البياني الفرعي"
|
||||
},
|
||||
"Comfy_Graph_ToggleWidgetPromotion": {
|
||||
"label": "تبديل ترقية عنصر الواجهة المحوم فوقه"
|
||||
},
|
||||
"Comfy_ToggleAssetAPI": {
|
||||
"label": "تجريبي: تمكين AssetAPI"
|
||||
},
|
||||
"Experimental_ToggleVueNodes": {
|
||||
"label": "تجريبي: تمكين عقد Vue"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "تبديل الشريط الجانبي للأصول",
|
||||
"tooltip": "الأصول"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1210,7 +1210,6 @@
|
||||
"Pin/Unpin Selected Nodes": "تثبيت/إلغاء تثبيت العقد المحددة",
|
||||
"Previous Opened Workflow": "سير العمل السابق المفتوح",
|
||||
"Publish": "نشر",
|
||||
"Queue Panel": "لوحة الانتظار",
|
||||
"Queue Prompt": "قائمة انتظار التعليمات",
|
||||
"Queue Prompt (Front)": "قائمة انتظار التعليمات (أمامي)",
|
||||
"Queue Selected Output Nodes": "قائمة انتظار عقد المخرجات المحددة",
|
||||
@@ -1670,18 +1669,6 @@
|
||||
},
|
||||
"openWorkflow": "فتح سير العمل من نظام الملفات المحلي",
|
||||
"queue": "قائمة الانتظار",
|
||||
"queueTab": {
|
||||
"backToAllTasks": "العودة إلى جميع المهام",
|
||||
"clearPendingTasks": "مسح المهام المعلقة",
|
||||
"containImagePreview": "ملء معاينة الصورة",
|
||||
"coverImagePreview": "تكييف معاينة الصورة",
|
||||
"filter": "تصفية النتائج",
|
||||
"filters": {
|
||||
"hideCached": "إخفاء المخزنة مؤقتًا",
|
||||
"hideCanceled": "إخفاء الملغاة"
|
||||
},
|
||||
"showFlatList": "عرض القائمة المسطحة"
|
||||
},
|
||||
"templates": "القوالب",
|
||||
"themeToggle": "تبديل المظهر",
|
||||
"workflowTab": {
|
||||
|
||||
@@ -221,6 +221,9 @@
|
||||
"Comfy_ToggleHelpCenter": {
|
||||
"label": "Help Center"
|
||||
},
|
||||
"Comfy_ToggleLinear": {
|
||||
"label": "toggle linear mode"
|
||||
},
|
||||
"Comfy_ToggleTheme": {
|
||||
"label": "Toggle Theme (Dark/Light)"
|
||||
},
|
||||
@@ -281,10 +284,6 @@
|
||||
"label": "Toggle Node Library Sidebar",
|
||||
"tooltip": "Node Library"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_queue": {
|
||||
"label": "Toggle Queue Sidebar",
|
||||
"tooltip": "Queue"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_workflows": {
|
||||
"label": "Toggle Workflows Sidebar",
|
||||
"tooltip": "Workflows"
|
||||
|
||||
@@ -402,6 +402,7 @@
|
||||
"Copy Image": "Copy Image",
|
||||
"Save Image": "Save Image",
|
||||
"Rename": "Rename",
|
||||
"RenameWidget": "Rename Widget",
|
||||
"Copy": "Copy",
|
||||
"Duplicate": "Duplicate",
|
||||
"Paste": "Paste",
|
||||
@@ -681,18 +682,6 @@
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"queueProgressOverlay": {
|
||||
"title": "Queue Progress",
|
||||
"total": "Total: {percent}",
|
||||
@@ -1113,6 +1102,7 @@
|
||||
"Experimental: Enable AssetAPI": "Experimental: Enable AssetAPI",
|
||||
"Canvas Performance": "Canvas Performance",
|
||||
"Help Center": "Help Center",
|
||||
"toggle linear mode": "toggle linear mode",
|
||||
"Toggle Theme (Dark/Light)": "Toggle Theme (Dark/Light)",
|
||||
"Undo": "Undo",
|
||||
"Open Sign In Dialog": "Open Sign In Dialog",
|
||||
@@ -1132,7 +1122,6 @@
|
||||
"Assets": "Assets",
|
||||
"Model Library": "Model Library",
|
||||
"Node Library": "Node Library",
|
||||
"Queue Panel": "Queue Panel",
|
||||
"Workflows": "Workflows"
|
||||
},
|
||||
"desktopMenu": {
|
||||
@@ -1414,6 +1403,7 @@
|
||||
"stable_cascade": "stable_cascade",
|
||||
"3d_models": "3d_models",
|
||||
"style_model": "style_model",
|
||||
"Topaz": "Topaz",
|
||||
"Tripo": "Tripo",
|
||||
"Veo": "Veo",
|
||||
"Vidu": "Vidu",
|
||||
@@ -2085,8 +2075,8 @@
|
||||
"failedToCreateNode": "Failed to create node. Please try again or check console for details.",
|
||||
"noModelsInFolder": "No {type} available in this folder",
|
||||
"searchAssetsPlaceholder": "Type to search...",
|
||||
"uploadModel": "Import model",
|
||||
"uploadModelFromCivitai": "Import a model from Civitai",
|
||||
"uploadModel": "Upload model",
|
||||
"uploadModelFromCivitai": "Upload a model from Civitai",
|
||||
"uploadModelFailedToRetrieveMetadata": "Failed to retrieve metadata. Please check the link and try again.",
|
||||
"onlyCivitaiUrlsSupported": "Only Civitai URLs are supported",
|
||||
"uploadModelDescription1": "Paste a Civitai model download link to add it to your library.",
|
||||
@@ -2103,16 +2093,16 @@
|
||||
"tags": "Tags",
|
||||
"tagsPlaceholder": "e.g., models, checkpoint",
|
||||
"tagsHelp": "Separate tags with commas",
|
||||
"upload": "Import",
|
||||
"uploadingModel": "Importing model...",
|
||||
"uploadSuccess": "Model imported successfully!",
|
||||
"uploadFailed": "Import failed",
|
||||
"upload": "Upload",
|
||||
"uploadingModel": "Uploading model...",
|
||||
"uploadSuccess": "Model uploaded successfully!",
|
||||
"uploadFailed": "Upload failed",
|
||||
"modelAssociatedWithLink": "The model associated with the link you provided:",
|
||||
"modelTypeSelectorLabel": "What type of model is this?",
|
||||
"modelTypeSelectorPlaceholder": "Select model type",
|
||||
"selectModelType": "Select model type",
|
||||
"notSureLeaveAsIs": "Not sure? Just leave this as is",
|
||||
"modelUploaded": "Model imported! 🎉",
|
||||
"modelUploaded": "Model uploaded! 🎉",
|
||||
"findInLibrary": "Find it in the {type} section of the models library.",
|
||||
"finish": "Finish",
|
||||
"allModels": "All Models",
|
||||
@@ -2132,7 +2122,7 @@
|
||||
"errorUnsafeVirusScan": "CivitAI detected malware or suspicious content in this file",
|
||||
"errorModelTypeNotSupported": "This model type is not supported",
|
||||
"errorUnknown": "An unexpected error occurred",
|
||||
"errorUploadFailed": "Failed to import asset. Please try again.",
|
||||
"errorUploadFailed": "Failed to upload asset. Please try again.",
|
||||
"ariaLabel": {
|
||||
"assetCard": "{name} - {type} asset",
|
||||
"loadingAsset": "Loading asset"
|
||||
|
||||
@@ -2061,6 +2061,11 @@
|
||||
"name": "batch_size",
|
||||
"tooltip": "The number of latent images in the batch."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmptyLatentImage": {
|
||||
@@ -2794,10 +2799,12 @@
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "positive"
|
||||
"name": "positive",
|
||||
"tooltip": null
|
||||
},
|
||||
"1": {
|
||||
"name": "negative"
|
||||
"name": "negative",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2819,10 +2826,12 @@
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "positive"
|
||||
"name": "positive",
|
||||
"tooltip": null
|
||||
},
|
||||
"1": {
|
||||
"name": "negative"
|
||||
"name": "negative",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -8841,7 +8850,7 @@
|
||||
}
|
||||
},
|
||||
"PreviewAny": {
|
||||
"display_name": "Preview Any",
|
||||
"display_name": "Preview as Text",
|
||||
"inputs": {
|
||||
"source": {
|
||||
"name": "source"
|
||||
@@ -11548,6 +11557,118 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"TopazImageEnhance": {
|
||||
"display_name": "Topaz Image Enhance",
|
||||
"description": "Industry-standard upscaling and image enhancement.",
|
||||
"inputs": {
|
||||
"model": {
|
||||
"name": "model"
|
||||
},
|
||||
"image": {
|
||||
"name": "image"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"tooltip": "Optional text prompt for creative upscaling guidance."
|
||||
},
|
||||
"subject_detection": {
|
||||
"name": "subject_detection"
|
||||
},
|
||||
"face_enhancement": {
|
||||
"name": "face_enhancement",
|
||||
"tooltip": "Enhance faces (if present) during processing."
|
||||
},
|
||||
"face_enhancement_creativity": {
|
||||
"name": "face_enhancement_creativity",
|
||||
"tooltip": "Set the creativity level for face enhancement."
|
||||
},
|
||||
"face_enhancement_strength": {
|
||||
"name": "face_enhancement_strength",
|
||||
"tooltip": "Controls how sharp enhanced faces are relative to the background."
|
||||
},
|
||||
"crop_to_fill": {
|
||||
"name": "crop_to_fill",
|
||||
"tooltip": "By default, the image is letterboxed when the output aspect ratio differs. Enable to crop the image to fill the output dimensions."
|
||||
},
|
||||
"output_width": {
|
||||
"name": "output_width",
|
||||
"tooltip": "Zero value means to calculate automatically (usually it will be original size or output_height if specified)."
|
||||
},
|
||||
"output_height": {
|
||||
"name": "output_height",
|
||||
"tooltip": "Zero value means to output in the same height as original or output width."
|
||||
},
|
||||
"creativity": {
|
||||
"name": "creativity"
|
||||
},
|
||||
"face_preservation": {
|
||||
"name": "face_preservation",
|
||||
"tooltip": "Preserve subjects' facial identity."
|
||||
},
|
||||
"color_preservation": {
|
||||
"name": "color_preservation",
|
||||
"tooltip": "Preserve the original colors."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"TopazVideoEnhance": {
|
||||
"display_name": "Topaz Video Enhance",
|
||||
"description": "Breathe new life into video with powerful upscaling and recovery technology.",
|
||||
"inputs": {
|
||||
"video": {
|
||||
"name": "video"
|
||||
},
|
||||
"upscaler_enabled": {
|
||||
"name": "upscaler_enabled"
|
||||
},
|
||||
"upscaler_model": {
|
||||
"name": "upscaler_model"
|
||||
},
|
||||
"upscaler_resolution": {
|
||||
"name": "upscaler_resolution"
|
||||
},
|
||||
"upscaler_creativity": {
|
||||
"name": "upscaler_creativity",
|
||||
"tooltip": "Creativity level (applies only to Starlight (Astra) Creative)."
|
||||
},
|
||||
"interpolation_enabled": {
|
||||
"name": "interpolation_enabled"
|
||||
},
|
||||
"interpolation_model": {
|
||||
"name": "interpolation_model"
|
||||
},
|
||||
"interpolation_slowmo": {
|
||||
"name": "interpolation_slowmo",
|
||||
"tooltip": "Slow-motion factor applied to the input video. For example, 2 makes the output twice as slow and doubles the duration."
|
||||
},
|
||||
"interpolation_frame_rate": {
|
||||
"name": "interpolation_frame_rate",
|
||||
"tooltip": "Output frame rate."
|
||||
},
|
||||
"interpolation_duplicate": {
|
||||
"name": "interpolation_duplicate",
|
||||
"tooltip": "Analyze the input for duplicate frames and remove them."
|
||||
},
|
||||
"interpolation_duplicate_threshold": {
|
||||
"name": "interpolation_duplicate_threshold",
|
||||
"tooltip": "Detection sensitivity for duplicate frames."
|
||||
},
|
||||
"dynamic_compression_level": {
|
||||
"name": "dynamic_compression_level",
|
||||
"tooltip": "CQP level."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"TorchCompileModel": {
|
||||
"display_name": "TorchCompileModel",
|
||||
"inputs": {
|
||||
@@ -12162,6 +12283,11 @@
|
||||
"octree_resolution": {
|
||||
"name": "octree_resolution"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeTiled": {
|
||||
@@ -12586,6 +12712,11 @@
|
||||
"threshold": {
|
||||
"name": "threshold"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VoxelToMeshBasic": {
|
||||
@@ -12597,6 +12728,11 @@
|
||||
"threshold": {
|
||||
"name": "threshold"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VPScheduler": {
|
||||
|
||||
@@ -335,11 +335,11 @@
|
||||
"name": "Validate workflows"
|
||||
},
|
||||
"Comfy_VueNodes_AutoScaleLayout": {
|
||||
"name": "Auto-scale layout (Nodes 2.0)",
|
||||
"name": "Auto-scale layout (Vue nodes)",
|
||||
"tooltip": "Automatically scale node positions when switching to Vue rendering to prevent overlap"
|
||||
},
|
||||
"Comfy_VueNodes_Enabled": {
|
||||
"name": "Modern Node Design (Nodes 2.0)",
|
||||
"name": "Modern Node Design (Vue Nodes)",
|
||||
"tooltip": "Modern: DOM-based rendering with enhanced interactivity, native browser features, and updated visual design. Classic: Traditional canvas rendering."
|
||||
},
|
||||
"Comfy_WidgetControlMode": {
|
||||
|
||||
@@ -1,43 +1,10 @@
|
||||
{
|
||||
"Comfy-Desktop_CheckForUpdates": {
|
||||
"label": "Buscar actualizaciones"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "Abrir carpeta de nodos personalizados"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenInputsFolder": {
|
||||
"label": "Abrir carpeta de entradas"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenLogsFolder": {
|
||||
"label": "Abrir carpeta de registros"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelConfig": {
|
||||
"label": "Abrir extra_model_paths.yaml"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelsFolder": {
|
||||
"label": "Abrir carpeta de modelos"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenOutputsFolder": {
|
||||
"label": "Abrir carpeta de salidas"
|
||||
},
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "Abrir herramientas de desarrollo"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "Guía de usuario de escritorio"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "Salir"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "Reinstalar"
|
||||
},
|
||||
"Comfy-Desktop_Restart": {
|
||||
"label": "Reiniciar"
|
||||
},
|
||||
"Comfy_3DViewer_Open3DViewer": {
|
||||
"label": "Abrir visor 3D (Beta) para el nodo seleccionado"
|
||||
},
|
||||
"Comfy_BrowseModelAssets": {
|
||||
"label": "Experimental: Explorar recursos de modelos"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "Explorar plantillas"
|
||||
},
|
||||
@@ -125,6 +92,9 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "Convertir selección en subgrafo"
|
||||
},
|
||||
"Comfy_Graph_EditSubgraphWidgets": {
|
||||
"label": "Editar widgets de subgráficos"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "Salir de subgrafo"
|
||||
},
|
||||
@@ -134,6 +104,9 @@
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "Agrupar nodos seleccionados"
|
||||
},
|
||||
"Comfy_Graph_ToggleWidgetPromotion": {
|
||||
"label": "Alternar promoción del widget sobre el que se pasa el cursor"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "Desempaquetar el subgrafo seleccionado"
|
||||
},
|
||||
@@ -239,6 +212,9 @@
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "Mostrar Diálogo de Configuraciones"
|
||||
},
|
||||
"Comfy_ToggleAssetAPI": {
|
||||
"label": "Experimental: Habilitar AssetAPI"
|
||||
},
|
||||
"Comfy_ToggleCanvasInfo": {
|
||||
"label": "Rendimiento del lienzo"
|
||||
},
|
||||
@@ -257,6 +233,9 @@
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "Cerrar sesión"
|
||||
},
|
||||
"Experimental_ToggleVueNodes": {
|
||||
"label": "Experimental: Habilitar nodos Vue"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "Cerrar Flujo de Trabajo Actual"
|
||||
},
|
||||
@@ -290,6 +269,10 @@
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "Alternar Modo de Enfoque"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "Alternar barra lateral de recursos",
|
||||
"tooltip": "Recursos"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_model-library": {
|
||||
"label": "Alternar Barra Lateral de Biblioteca de Modelos",
|
||||
"tooltip": "Biblioteca de Modelos"
|
||||
@@ -298,31 +281,8 @@
|
||||
"label": "Alternar Barra Lateral de Biblioteca de Nodos",
|
||||
"tooltip": "Biblioteca de Nodos"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_queue": {
|
||||
"label": "Alternar Barra Lateral de Cola",
|
||||
"tooltip": "Cola"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_workflows": {
|
||||
"label": "Alternar Barra Lateral de Flujos de Trabajo",
|
||||
"tooltip": "Flujos de Trabajo"
|
||||
},
|
||||
"Comfy_BrowseModelAssets": {
|
||||
"label": "Experimental: Explorar recursos de modelos"
|
||||
},
|
||||
"Comfy_Graph_EditSubgraphWidgets": {
|
||||
"label": "Editar widgets de subgráficos"
|
||||
},
|
||||
"Comfy_Graph_ToggleWidgetPromotion": {
|
||||
"label": "Alternar promoción del widget sobre el que se pasa el cursor"
|
||||
},
|
||||
"Comfy_ToggleAssetAPI": {
|
||||
"label": "Experimental: Habilitar AssetAPI"
|
||||
},
|
||||
"Experimental_ToggleVueNodes": {
|
||||
"label": "Experimental: Habilitar nodos Vue"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "Alternar barra lateral de recursos",
|
||||
"tooltip": "Recursos"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1210,7 +1210,6 @@
|
||||
"Pin/Unpin Selected Nodes": "Anclar/Desanclar nodos seleccionados",
|
||||
"Previous Opened Workflow": "Flujo de trabajo abierto anterior",
|
||||
"Publish": "Publicar",
|
||||
"Queue Panel": "Panel de Cola",
|
||||
"Queue Prompt": "Indicador de cola",
|
||||
"Queue Prompt (Front)": "Indicador de cola (Frente)",
|
||||
"Queue Selected Output Nodes": "Encolar nodos de salida seleccionados",
|
||||
@@ -1670,18 +1669,6 @@
|
||||
},
|
||||
"openWorkflow": "Abrir flujo de trabajo en el sistema de archivos local",
|
||||
"queue": "Cola",
|
||||
"queueTab": {
|
||||
"backToAllTasks": "Volver a todas las tareas",
|
||||
"clearPendingTasks": "Borrar tareas pendientes",
|
||||
"containImagePreview": "Llenar vista previa de la imagen",
|
||||
"coverImagePreview": "Ajustar vista previa de la imagen",
|
||||
"filter": "Filtrar salidas",
|
||||
"filters": {
|
||||
"hideCached": "Ocultar en caché",
|
||||
"hideCanceled": "Ocultar cancelados"
|
||||
},
|
||||
"showFlatList": "Mostrar lista plana"
|
||||
},
|
||||
"templates": "Plantillas",
|
||||
"themeToggle": "Cambiar tema",
|
||||
"workflowTab": {
|
||||
|
||||
@@ -1,43 +1,10 @@
|
||||
{
|
||||
"Comfy-Desktop_CheckForUpdates": {
|
||||
"label": "Vérifier les mises à jour"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "Ouvrir le dossier des nœuds personnalisés"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenInputsFolder": {
|
||||
"label": "Ouvrir le dossier des entrées"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenLogsFolder": {
|
||||
"label": "Ouvrir le dossier des journaux"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelConfig": {
|
||||
"label": "Ouvrir extra_model_paths.yaml"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelsFolder": {
|
||||
"label": "Ouvrir le dossier des modèles"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenOutputsFolder": {
|
||||
"label": "Ouvrir le dossier des sorties"
|
||||
},
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "Ouvrir les outils de développement"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "Guide de l'utilisateur du bureau"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "Quitter"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "Réinstaller"
|
||||
},
|
||||
"Comfy-Desktop_Restart": {
|
||||
"label": "Redémarrer"
|
||||
},
|
||||
"Comfy_3DViewer_Open3DViewer": {
|
||||
"label": "Ouvrir le visualiseur 3D (bêta) pour le nœud sélectionné"
|
||||
},
|
||||
"Comfy_BrowseModelAssets": {
|
||||
"label": "Expérimental : Parcourir les ressources de modèles"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "Parcourir les modèles"
|
||||
},
|
||||
@@ -125,6 +92,9 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "Convertir la sélection en sous-graphe"
|
||||
},
|
||||
"Comfy_Graph_EditSubgraphWidgets": {
|
||||
"label": "Modifier les widgets de sous-graphe"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "Quitter le sous-graphe"
|
||||
},
|
||||
@@ -134,6 +104,9 @@
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "Grouper les nœuds sélectionnés"
|
||||
},
|
||||
"Comfy_Graph_ToggleWidgetPromotion": {
|
||||
"label": "Activer/désactiver la promotion du widget survolé"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "Décompresser le sous-graphe sélectionné"
|
||||
},
|
||||
@@ -239,6 +212,9 @@
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "Afficher la boîte de dialogue des paramètres"
|
||||
},
|
||||
"Comfy_ToggleAssetAPI": {
|
||||
"label": "Expérimental : Activer AssetAPI"
|
||||
},
|
||||
"Comfy_ToggleCanvasInfo": {
|
||||
"label": "Performance du canvas"
|
||||
},
|
||||
@@ -257,6 +233,9 @@
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "Se déconnecter"
|
||||
},
|
||||
"Experimental_ToggleVueNodes": {
|
||||
"label": "Expérimental : Activer les nœuds Vue"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "Fermer le flux de travail actuel"
|
||||
},
|
||||
@@ -290,6 +269,10 @@
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "Basculer le mode focus"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "Afficher/Masquer la barre latérale des ressources",
|
||||
"tooltip": "Ressources"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_model-library": {
|
||||
"label": "Basculer la barre latérale de la bibliothèque de modèles",
|
||||
"tooltip": "Bibliothèque de modèles"
|
||||
@@ -298,31 +281,8 @@
|
||||
"label": "Basculer la barre latérale de la bibliothèque de nœuds",
|
||||
"tooltip": "Bibliothèque de nœuds"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_queue": {
|
||||
"label": "Basculer la barre latérale de la file d'attente",
|
||||
"tooltip": "File d'attente"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_workflows": {
|
||||
"label": "Basculer la barre latérale des flux de travail",
|
||||
"tooltip": "Flux de travail"
|
||||
},
|
||||
"Comfy_BrowseModelAssets": {
|
||||
"label": "Expérimental : Parcourir les ressources de modèles"
|
||||
},
|
||||
"Comfy_Graph_EditSubgraphWidgets": {
|
||||
"label": "Modifier les widgets de sous-graphe"
|
||||
},
|
||||
"Comfy_Graph_ToggleWidgetPromotion": {
|
||||
"label": "Activer/désactiver la promotion du widget survolé"
|
||||
},
|
||||
"Comfy_ToggleAssetAPI": {
|
||||
"label": "Expérimental : Activer AssetAPI"
|
||||
},
|
||||
"Experimental_ToggleVueNodes": {
|
||||
"label": "Expérimental : Activer les nœuds Vue"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "Afficher/Masquer la barre latérale des ressources",
|
||||
"tooltip": "Ressources"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1210,7 +1210,6 @@
|
||||
"Pin/Unpin Selected Nodes": "Épingler/Désépingler les nœuds sélectionnés",
|
||||
"Previous Opened Workflow": "Flux de travail ouvert précédent",
|
||||
"Publish": "Publier",
|
||||
"Queue Panel": "Panneau de file d'attente",
|
||||
"Queue Prompt": "Invite de file d'attente",
|
||||
"Queue Prompt (Front)": "Invite de file d'attente (Front)",
|
||||
"Queue Selected Output Nodes": "Mettre en file d’attente les nœuds de sortie sélectionnés",
|
||||
@@ -1670,18 +1669,6 @@
|
||||
},
|
||||
"openWorkflow": "Ouvrir le flux de travail dans le système de fichiers local",
|
||||
"queue": "File d'attente",
|
||||
"queueTab": {
|
||||
"backToAllTasks": "Retour à toutes les tâches",
|
||||
"clearPendingTasks": "Effacer les tâches en attente",
|
||||
"containImagePreview": "Remplir l'aperçu de l'image",
|
||||
"coverImagePreview": "Adapter l'aperçu de l'image",
|
||||
"filter": "Filtrer les sorties",
|
||||
"filters": {
|
||||
"hideCached": "Masquer le cache",
|
||||
"hideCanceled": "Masquer les annulations"
|
||||
},
|
||||
"showFlatList": "Afficher la liste plate"
|
||||
},
|
||||
"templates": "Modèles",
|
||||
"themeToggle": "Basculer le thème",
|
||||
"workflowTab": {
|
||||
|
||||
@@ -1,43 +1,10 @@
|
||||
{
|
||||
"Comfy-Desktop_CheckForUpdates": {
|
||||
"label": "更新を確認する"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "カスタムノードフォルダを開く"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenInputsFolder": {
|
||||
"label": "入力フォルダを開く"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenLogsFolder": {
|
||||
"label": "ログフォルダを開く"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelConfig": {
|
||||
"label": "extra_model_paths.yamlを開く"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelsFolder": {
|
||||
"label": "モデルフォルダを開く"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenOutputsFolder": {
|
||||
"label": "出力フォルダを開く"
|
||||
},
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "DevToolsを開く"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "デスクトップユーザーガイド"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "終了"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "再インストール"
|
||||
},
|
||||
"Comfy-Desktop_Restart": {
|
||||
"label": "再起動"
|
||||
},
|
||||
"Comfy_3DViewer_Open3DViewer": {
|
||||
"label": "選択したノードの3Dビューアー(ベータ)を開く"
|
||||
},
|
||||
"Comfy_BrowseModelAssets": {
|
||||
"label": "実験的: モデルアセットを参照"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "テンプレートを参照"
|
||||
},
|
||||
@@ -125,6 +92,9 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "選択範囲をサブグラフに変換"
|
||||
},
|
||||
"Comfy_Graph_EditSubgraphWidgets": {
|
||||
"label": "サブグラフウィジェットを編集"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "サブグラフを終了"
|
||||
},
|
||||
@@ -134,6 +104,9 @@
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "選択したノードをグループ化"
|
||||
},
|
||||
"Comfy_Graph_ToggleWidgetPromotion": {
|
||||
"label": "ホバー中のウィジェットの優先表示を切り替え"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "選択したサブグラフを展開"
|
||||
},
|
||||
@@ -239,6 +212,9 @@
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "設定ダイアログを表示"
|
||||
},
|
||||
"Comfy_ToggleAssetAPI": {
|
||||
"label": "実験的: AssetAPIを有効化"
|
||||
},
|
||||
"Comfy_ToggleCanvasInfo": {
|
||||
"label": "キャンバスパフォーマンス"
|
||||
},
|
||||
@@ -257,6 +233,9 @@
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "サインアウト"
|
||||
},
|
||||
"Experimental_ToggleVueNodes": {
|
||||
"label": "実験的: Vueノードを有効化"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "現在のワークフローを閉じる"
|
||||
},
|
||||
@@ -290,6 +269,10 @@
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "フォーカスモードの切り替え"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "アセットサイドバーの表示切り替え",
|
||||
"tooltip": "アセット"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_model-library": {
|
||||
"label": "モデルライブラリサイドバーの切り替え",
|
||||
"tooltip": "モデルライブラリ"
|
||||
@@ -298,31 +281,8 @@
|
||||
"label": "ノードライブラリサイドバーの切り替え",
|
||||
"tooltip": "ノードライブラリ"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_queue": {
|
||||
"label": "キューサイドバーの切り替え",
|
||||
"tooltip": "キュー"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_workflows": {
|
||||
"label": "ワークフローサイドバーの切り替え",
|
||||
"tooltip": "ワークフロー"
|
||||
},
|
||||
"Comfy_BrowseModelAssets": {
|
||||
"label": "実験的: モデルアセットを参照"
|
||||
},
|
||||
"Comfy_Graph_EditSubgraphWidgets": {
|
||||
"label": "サブグラフウィジェットを編集"
|
||||
},
|
||||
"Comfy_Graph_ToggleWidgetPromotion": {
|
||||
"label": "ホバー中のウィジェットの優先表示を切り替え"
|
||||
},
|
||||
"Comfy_ToggleAssetAPI": {
|
||||
"label": "実験的: AssetAPIを有効化"
|
||||
},
|
||||
"Experimental_ToggleVueNodes": {
|
||||
"label": "実験的: Vueノードを有効化"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "アセットサイドバーの表示切り替え",
|
||||
"tooltip": "アセット"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1210,7 +1210,6 @@
|
||||
"Pin/Unpin Selected Nodes": "選択したノードのピン留め/ピン留め解除",
|
||||
"Previous Opened Workflow": "前に開いたワークフロー",
|
||||
"Publish": "公開",
|
||||
"Queue Panel": "キューパネル",
|
||||
"Queue Prompt": "キューのプロンプト",
|
||||
"Queue Prompt (Front)": "キューのプロンプト (前面)",
|
||||
"Queue Selected Output Nodes": "選択した出力ノードをキューに追加",
|
||||
@@ -1670,18 +1669,6 @@
|
||||
},
|
||||
"openWorkflow": "ローカルでワークフローを開く",
|
||||
"queue": "キュー",
|
||||
"queueTab": {
|
||||
"backToAllTasks": "すべてのタスクに戻る",
|
||||
"clearPendingTasks": "保留中のタスクをクリア",
|
||||
"containImagePreview": "画像プレビューを含める",
|
||||
"coverImagePreview": "画像プレビューに合わせる",
|
||||
"filter": "出力をフィルタ",
|
||||
"filters": {
|
||||
"hideCached": "キャッシュを非表示",
|
||||
"hideCanceled": "キャンセル済みを非表示"
|
||||
},
|
||||
"showFlatList": "フラットリストを表示"
|
||||
},
|
||||
"templates": "テンプレート",
|
||||
"themeToggle": "テーマを切り替え",
|
||||
"workflowTab": {
|
||||
|
||||
@@ -1,43 +1,10 @@
|
||||
{
|
||||
"Comfy-Desktop_CheckForUpdates": {
|
||||
"label": "업데이트 확인"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "커스텀 노드 폴더 열기"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenInputsFolder": {
|
||||
"label": "입력 폴더 열기"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenLogsFolder": {
|
||||
"label": "로그 폴더 열기"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelConfig": {
|
||||
"label": "extra_model_paths.yaml 열기"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelsFolder": {
|
||||
"label": "모델 폴더 열기"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenOutputsFolder": {
|
||||
"label": "출력 폴더 열기"
|
||||
},
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "DevTools 열기"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "데스크톱 사용자 가이드"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "종료"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "재설치"
|
||||
},
|
||||
"Comfy-Desktop_Restart": {
|
||||
"label": "재시작"
|
||||
},
|
||||
"Comfy_3DViewer_Open3DViewer": {
|
||||
"label": "선택한 노드에 대해 3D 뷰어(베타) 열기"
|
||||
},
|
||||
"Comfy_BrowseModelAssets": {
|
||||
"label": "실험적: 모델 에셋 탐색"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "템플릿 탐색"
|
||||
},
|
||||
@@ -125,6 +92,9 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "선택 영역을 서브그래프로 변환"
|
||||
},
|
||||
"Comfy_Graph_EditSubgraphWidgets": {
|
||||
"label": "서브그래프 위젯 편집"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "서브그래프 나가기"
|
||||
},
|
||||
@@ -134,6 +104,9 @@
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "선택한 노드 그룹화"
|
||||
},
|
||||
"Comfy_Graph_ToggleWidgetPromotion": {
|
||||
"label": "호버링된 위젯 프로모션 전환"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "선택한 서브그래프 묶음 풀기"
|
||||
},
|
||||
@@ -239,6 +212,9 @@
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "설정 대화상자 보기"
|
||||
},
|
||||
"Comfy_ToggleAssetAPI": {
|
||||
"label": "실험적: AssetAPI 활성화"
|
||||
},
|
||||
"Comfy_ToggleCanvasInfo": {
|
||||
"label": "캔버스 성능"
|
||||
},
|
||||
@@ -257,6 +233,9 @@
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "로그아웃"
|
||||
},
|
||||
"Experimental_ToggleVueNodes": {
|
||||
"label": "실험적: Vue 노드 활성화"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "현재 워크플로 닫기"
|
||||
},
|
||||
@@ -290,6 +269,10 @@
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "포커스 모드 토글"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "에셋 사이드바 전환",
|
||||
"tooltip": "에셋"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_model-library": {
|
||||
"label": "모델 라이브러리 사이드바 토글",
|
||||
"tooltip": "모델 라이브러리"
|
||||
@@ -298,31 +281,8 @@
|
||||
"label": "노드 라이브러리 사이드바 토글",
|
||||
"tooltip": "노드 라이브러리"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_queue": {
|
||||
"label": "실행 큐 사이드바 토글",
|
||||
"tooltip": "실행 큐"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_workflows": {
|
||||
"label": "워크플로 사이드바 토글",
|
||||
"tooltip": "워크플로"
|
||||
},
|
||||
"Comfy_BrowseModelAssets": {
|
||||
"label": "실험적: 모델 에셋 탐색"
|
||||
},
|
||||
"Comfy_Graph_EditSubgraphWidgets": {
|
||||
"label": "서브그래프 위젯 편집"
|
||||
},
|
||||
"Comfy_Graph_ToggleWidgetPromotion": {
|
||||
"label": "호버링된 위젯 프로모션 전환"
|
||||
},
|
||||
"Comfy_ToggleAssetAPI": {
|
||||
"label": "실험적: AssetAPI 활성화"
|
||||
},
|
||||
"Experimental_ToggleVueNodes": {
|
||||
"label": "실험적: Vue 노드 활성화"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "에셋 사이드바 전환",
|
||||
"tooltip": "에셋"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1210,7 +1210,6 @@
|
||||
"Pin/Unpin Selected Nodes": "선택한 노드 고정/고정 해제",
|
||||
"Previous Opened Workflow": "이전 열린 워크플로",
|
||||
"Publish": "게시",
|
||||
"Queue Panel": "큐 패널",
|
||||
"Queue Prompt": "실행 대기열에 프롬프트 추가",
|
||||
"Queue Prompt (Front)": "실행 대기열 맨 앞에 프롬프트 추가",
|
||||
"Queue Selected Output Nodes": "선택한 출력 노드 대기열에 추가",
|
||||
@@ -1670,18 +1669,6 @@
|
||||
},
|
||||
"openWorkflow": "로컬 파일 시스템에서 워크플로 열기",
|
||||
"queue": "실행 대기열",
|
||||
"queueTab": {
|
||||
"backToAllTasks": "모든 작업으로 돌아가기",
|
||||
"clearPendingTasks": "보류 중인 작업 지우기",
|
||||
"containImagePreview": "이미지 미리보기 채우기",
|
||||
"coverImagePreview": "이미지 미리보기 맞추기",
|
||||
"filter": "출력 필터",
|
||||
"filters": {
|
||||
"hideCached": "캐시 숨기기",
|
||||
"hideCanceled": "취소된 작업 숨기기"
|
||||
},
|
||||
"showFlatList": "평면 목록 표시"
|
||||
},
|
||||
"templates": "템플릿",
|
||||
"themeToggle": "테마 전환",
|
||||
"workflowTab": {
|
||||
|
||||
@@ -1,43 +1,10 @@
|
||||
{
|
||||
"Comfy-Desktop_CheckForUpdates": {
|
||||
"label": "Проверить наличие обновлений"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "Открыть папку пользовательских нод"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenInputsFolder": {
|
||||
"label": "Открыть папку входных данных"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenLogsFolder": {
|
||||
"label": "Открыть папку логов"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelConfig": {
|
||||
"label": "Открыть extra_model_paths.yaml"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelsFolder": {
|
||||
"label": "Открыть папку моделей"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenOutputsFolder": {
|
||||
"label": "Открыть папку результатов"
|
||||
},
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "Открыть инструменты разработчика"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "Руководство пользователя для рабочего стола"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "Выйти"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "Переустановить"
|
||||
},
|
||||
"Comfy-Desktop_Restart": {
|
||||
"label": "Перезагрузить"
|
||||
},
|
||||
"Comfy_3DViewer_Open3DViewer": {
|
||||
"label": "Открыть 3D-просмотрщик (бета) для выбранного узла"
|
||||
},
|
||||
"Comfy_BrowseModelAssets": {
|
||||
"label": "Экспериментально: Просмотр ресурсов моделей"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "Просмотр шаблонов"
|
||||
},
|
||||
@@ -125,6 +92,9 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "Преобразовать выделенное в подграф"
|
||||
},
|
||||
"Comfy_Graph_EditSubgraphWidgets": {
|
||||
"label": "Редактировать виджеты подграфов"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "Выйти из подграфа"
|
||||
},
|
||||
@@ -134,6 +104,9 @@
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "Группировать выбранные ноды"
|
||||
},
|
||||
"Comfy_Graph_ToggleWidgetPromotion": {
|
||||
"label": "Переключить продвижение наведенного виджета"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "Распаковать выбранный подграф"
|
||||
},
|
||||
@@ -239,6 +212,9 @@
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "Показать диалог настроек"
|
||||
},
|
||||
"Comfy_ToggleAssetAPI": {
|
||||
"label": "Экспериментально: Включить AssetAPI"
|
||||
},
|
||||
"Comfy_ToggleCanvasInfo": {
|
||||
"label": "Производительность холста"
|
||||
},
|
||||
@@ -257,6 +233,9 @@
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "Выйти"
|
||||
},
|
||||
"Experimental_ToggleVueNodes": {
|
||||
"label": "Экспериментально: Включить Vue узлы"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "Закрыть текущий рабочий процесс"
|
||||
},
|
||||
@@ -290,6 +269,10 @@
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "Переключить режим фокуса"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "Переключить боковую панель ресурсов",
|
||||
"tooltip": "Ресурсы"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_model-library": {
|
||||
"label": "Переключить боковую панель библиотеки моделей",
|
||||
"tooltip": "Библиотека моделей"
|
||||
@@ -298,31 +281,8 @@
|
||||
"label": "Переключить боковую панель библиотеки нод",
|
||||
"tooltip": "Библиотека нод"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_queue": {
|
||||
"label": "Переключить боковую панель очереди",
|
||||
"tooltip": "Очередь"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_workflows": {
|
||||
"label": "Переключить боковую панель рабочих процессов",
|
||||
"tooltip": "Рабочие процессы"
|
||||
},
|
||||
"Comfy_BrowseModelAssets": {
|
||||
"label": "Экспериментально: Просмотр ресурсов моделей"
|
||||
},
|
||||
"Comfy_Graph_EditSubgraphWidgets": {
|
||||
"label": "Редактировать виджеты подграфов"
|
||||
},
|
||||
"Comfy_Graph_ToggleWidgetPromotion": {
|
||||
"label": "Переключить продвижение наведенного виджета"
|
||||
},
|
||||
"Comfy_ToggleAssetAPI": {
|
||||
"label": "Экспериментально: Включить AssetAPI"
|
||||
},
|
||||
"Experimental_ToggleVueNodes": {
|
||||
"label": "Экспериментально: Включить Vue узлы"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "Переключить боковую панель ресурсов",
|
||||
"tooltip": "Ресурсы"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1210,7 +1210,6 @@
|
||||
"Pin/Unpin Selected Nodes": "Закрепить/открепить выбранные ноды",
|
||||
"Previous Opened Workflow": "Предыдущий открытый рабочий процесс",
|
||||
"Publish": "Опубликовать",
|
||||
"Queue Panel": "Панель очереди",
|
||||
"Queue Prompt": "Запрос в очереди",
|
||||
"Queue Prompt (Front)": "Запрос в очереди (спереди)",
|
||||
"Queue Selected Output Nodes": "Добавить выбранные выходные узлы в очередь",
|
||||
@@ -1670,18 +1669,6 @@
|
||||
},
|
||||
"openWorkflow": "Открыть рабочий процесс в локальной файловой системе",
|
||||
"queue": "Очередь",
|
||||
"queueTab": {
|
||||
"backToAllTasks": "Вернуться ко всем задачам",
|
||||
"clearPendingTasks": "Очистить отложенные задачи",
|
||||
"containImagePreview": "Предпросмотр заливающего изображения",
|
||||
"coverImagePreview": "Предпросмотр подходящего изображения",
|
||||
"filter": "Фильтровать выводы",
|
||||
"filters": {
|
||||
"hideCached": "Скрыть кэшированные",
|
||||
"hideCanceled": "Скрыть отмененные"
|
||||
},
|
||||
"showFlatList": "Показать плоский список"
|
||||
},
|
||||
"templates": "Шаблоны",
|
||||
"themeToggle": "Переключить тему",
|
||||
"workflowTab": {
|
||||
|
||||
@@ -1,43 +1,10 @@
|
||||
{
|
||||
"Comfy-Desktop_CheckForUpdates": {
|
||||
"label": "Güncellemeleri Kontrol Et"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "Özel Düğümler Klasörünü Aç"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenInputsFolder": {
|
||||
"label": "Girişler Klasörünü Aç"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenLogsFolder": {
|
||||
"label": "Kayıtlar Klasörünü Aç"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelConfig": {
|
||||
"label": "extra_model_paths.yaml dosyasını aç"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelsFolder": {
|
||||
"label": "Modeller Klasörünü Aç"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenOutputsFolder": {
|
||||
"label": "Çıktılar Klasörünü Aç"
|
||||
},
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "Geliştirici Araçlarını Aç"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "Masaüstü Kullanıcı Kılavuzu"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "Çık"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "Yeniden Yükle"
|
||||
},
|
||||
"Comfy-Desktop_Restart": {
|
||||
"label": "Yeniden Başlat"
|
||||
},
|
||||
"Comfy_3DViewer_Open3DViewer": {
|
||||
"label": "Seçili Düğüm için 3D Görüntüleyiciyi (Beta) Aç"
|
||||
},
|
||||
"Comfy_BrowseModelAssets": {
|
||||
"label": "Deneysel: Model Varlıklarını Gözat"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "Şablonlara Gözat"
|
||||
},
|
||||
@@ -125,6 +92,9 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "Seçimi Alt Grafiğe Dönüştür"
|
||||
},
|
||||
"Comfy_Graph_EditSubgraphWidgets": {
|
||||
"label": "Alt Grafik Bileşenlerini Düzenle"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "Alt Grafikten Çık"
|
||||
},
|
||||
@@ -134,6 +104,9 @@
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "Seçili Düğümleri Gruplandır"
|
||||
},
|
||||
"Comfy_Graph_ToggleWidgetPromotion": {
|
||||
"label": "Vurgulanan bileşenin önceliğini değiştir"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "Seçili Alt Grafiği Aç"
|
||||
},
|
||||
@@ -239,6 +212,9 @@
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "Ayarlar İletişim Kutusunu Göster"
|
||||
},
|
||||
"Comfy_ToggleAssetAPI": {
|
||||
"label": "Deneysel: AssetAPI'yi Etkinleştir"
|
||||
},
|
||||
"Comfy_ToggleCanvasInfo": {
|
||||
"label": "Tuval Performansı"
|
||||
},
|
||||
@@ -257,6 +233,9 @@
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "Çıkış Yap"
|
||||
},
|
||||
"Experimental_ToggleVueNodes": {
|
||||
"label": "Deneysel: Vue Düğümlerini Etkinleştir"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "Mevcut İş Akışını Kapat"
|
||||
},
|
||||
@@ -290,6 +269,10 @@
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "Odak Modunu Aç/Kapat"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "Varlıklar Kenar Çubuğunu Aç/Kapat",
|
||||
"tooltip": "Varlıklar"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_model-library": {
|
||||
"label": "Model Kütüphanesi Kenar Çubuğunu Aç/Kapat",
|
||||
"tooltip": "Model Kütüphanesi"
|
||||
@@ -298,31 +281,8 @@
|
||||
"label": "Düğüm Kütüphanesi Kenar Çubuğunu Aç/Kapat",
|
||||
"tooltip": "Düğüm Kütüphanesi"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_queue": {
|
||||
"label": "Kuyruk Kenar Çubuğunu Aç/Kapat",
|
||||
"tooltip": "Kuyruk"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_workflows": {
|
||||
"label": "İş Akışları Kenar Çubuğunu Aç/Kapat",
|
||||
"tooltip": "İş Akışları"
|
||||
},
|
||||
"Comfy_BrowseModelAssets": {
|
||||
"label": "Deneysel: Model Varlıklarını Gözat"
|
||||
},
|
||||
"Comfy_Graph_EditSubgraphWidgets": {
|
||||
"label": "Alt Grafik Bileşenlerini Düzenle"
|
||||
},
|
||||
"Comfy_Graph_ToggleWidgetPromotion": {
|
||||
"label": "Vurgulanan bileşenin önceliğini değiştir"
|
||||
},
|
||||
"Comfy_ToggleAssetAPI": {
|
||||
"label": "Deneysel: AssetAPI'yi Etkinleştir"
|
||||
},
|
||||
"Experimental_ToggleVueNodes": {
|
||||
"label": "Deneysel: Vue Düğümlerini Etkinleştir"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "Varlıklar Kenar Çubuğunu Aç/Kapat",
|
||||
"tooltip": "Varlıklar"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1210,7 +1210,6 @@
|
||||
"Pin/Unpin Selected Nodes": "Seçili Düğümleri Sabitle/Kaldır",
|
||||
"Previous Opened Workflow": "Önceki Açılan İş Akışı",
|
||||
"Publish": "Yayınla",
|
||||
"Queue Panel": "Kuyruk Paneli",
|
||||
"Queue Prompt": "İstemi Kuyruğa Al",
|
||||
"Queue Prompt (Front)": "İstemi Kuyruğa Al (Ön)",
|
||||
"Queue Selected Output Nodes": "Seçili Çıktı Düğümlerini Kuyruğa Al",
|
||||
@@ -1670,18 +1669,6 @@
|
||||
},
|
||||
"openWorkflow": "Yerel dosya sisteminde iş akışını aç",
|
||||
"queue": "Kuyruk",
|
||||
"queueTab": {
|
||||
"backToAllTasks": "Tüm Görevlere Geri Dön",
|
||||
"clearPendingTasks": "Bekleyen Görevleri Temizle",
|
||||
"containImagePreview": "Resim Önizlemesini Doldur",
|
||||
"coverImagePreview": "Resim Önizlemesine Sığdır",
|
||||
"filter": "Çıktıları Filtrele",
|
||||
"filters": {
|
||||
"hideCached": "Önbelleğe Alınanları Gizle",
|
||||
"hideCanceled": "İptal Edilenleri Gizle"
|
||||
},
|
||||
"showFlatList": "Düz Listeyi Göster"
|
||||
},
|
||||
"templates": "Şablonlar",
|
||||
"themeToggle": "Temayı Değiştir",
|
||||
"workflowTab": {
|
||||
|
||||
@@ -1,43 +1,10 @@
|
||||
{
|
||||
"Comfy-Desktop_CheckForUpdates": {
|
||||
"label": "檢查更新"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "開啟自訂節點資料夾"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenInputsFolder": {
|
||||
"label": "開啟輸入資料夾"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenLogsFolder": {
|
||||
"label": "開啟日誌資料夾"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelConfig": {
|
||||
"label": "開啟 extra_model_paths.yaml"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelsFolder": {
|
||||
"label": "開啟模型資料夾"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenOutputsFolder": {
|
||||
"label": "開啟輸出資料夾"
|
||||
},
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "開啟開發者工具"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "桌面版使用指南"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "退出"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "重新安裝"
|
||||
},
|
||||
"Comfy-Desktop_Restart": {
|
||||
"label": "重新啟動"
|
||||
},
|
||||
"Comfy_3DViewer_Open3DViewer": {
|
||||
"label": "為選取的節點開啟 3D 檢視器(Beta)"
|
||||
},
|
||||
"Comfy_BrowseModelAssets": {
|
||||
"label": "實驗性:瀏覽模型資源"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "瀏覽範本"
|
||||
},
|
||||
@@ -125,6 +92,9 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "將選取內容轉換為子圖"
|
||||
},
|
||||
"Comfy_Graph_EditSubgraphWidgets": {
|
||||
"label": "編輯子圖表小工具"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "離開子圖"
|
||||
},
|
||||
@@ -134,6 +104,9 @@
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "群組所選節點"
|
||||
},
|
||||
"Comfy_Graph_ToggleWidgetPromotion": {
|
||||
"label": "切換懸停小工具的提升"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "解開所選子圖"
|
||||
},
|
||||
@@ -239,6 +212,9 @@
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "顯示設定對話框"
|
||||
},
|
||||
"Comfy_ToggleAssetAPI": {
|
||||
"label": "實驗性:啟用 AssetAPI"
|
||||
},
|
||||
"Comfy_ToggleCanvasInfo": {
|
||||
"label": "畫布效能"
|
||||
},
|
||||
@@ -257,6 +233,9 @@
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "登出"
|
||||
},
|
||||
"Experimental_ToggleVueNodes": {
|
||||
"label": "實驗性:啟用 Vue 節點"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "關閉當前工作流程"
|
||||
},
|
||||
@@ -290,6 +269,10 @@
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "切換專注模式"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "切換資源側邊欄",
|
||||
"tooltip": "資源"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_model-library": {
|
||||
"label": "切換模型庫側邊欄",
|
||||
"tooltip": "模型庫"
|
||||
@@ -298,31 +281,8 @@
|
||||
"label": "切換節點庫側邊欄",
|
||||
"tooltip": "節點庫"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_queue": {
|
||||
"label": "切換佇列側邊欄",
|
||||
"tooltip": "佇列"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_workflows": {
|
||||
"label": "切換工作流程側邊欄",
|
||||
"tooltip": "工作流程"
|
||||
},
|
||||
"Comfy_BrowseModelAssets": {
|
||||
"label": "實驗性:瀏覽模型資源"
|
||||
},
|
||||
"Comfy_Graph_EditSubgraphWidgets": {
|
||||
"label": "編輯子圖表小工具"
|
||||
},
|
||||
"Comfy_Graph_ToggleWidgetPromotion": {
|
||||
"label": "切換懸停小工具的提升"
|
||||
},
|
||||
"Comfy_ToggleAssetAPI": {
|
||||
"label": "實驗性:啟用 AssetAPI"
|
||||
},
|
||||
"Experimental_ToggleVueNodes": {
|
||||
"label": "實驗性:啟用 Vue 節點"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "切換資源側邊欄",
|
||||
"tooltip": "資源"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1210,7 +1210,6 @@
|
||||
"Pin/Unpin Selected Nodes": "釘選/取消釘選選取節點",
|
||||
"Previous Opened Workflow": "上一個已開啟的工作流程",
|
||||
"Publish": "發佈",
|
||||
"Queue Panel": "佇列面板",
|
||||
"Queue Prompt": "加入提示至佇列",
|
||||
"Queue Prompt (Front)": "將提示加入佇列前端",
|
||||
"Queue Selected Output Nodes": "將選取的輸出節點加入佇列",
|
||||
@@ -1670,18 +1669,6 @@
|
||||
},
|
||||
"openWorkflow": "在本機檔案系統中開啟工作流程",
|
||||
"queue": "佇列",
|
||||
"queueTab": {
|
||||
"backToAllTasks": "返回所有任務",
|
||||
"clearPendingTasks": "清除待處理任務",
|
||||
"containImagePreview": "填滿圖片預覽",
|
||||
"coverImagePreview": "適合圖片預覽",
|
||||
"filter": "篩選輸出",
|
||||
"filters": {
|
||||
"hideCached": "隱藏快取",
|
||||
"hideCanceled": "隱藏已取消"
|
||||
},
|
||||
"showFlatList": "顯示平面清單"
|
||||
},
|
||||
"templates": "範本",
|
||||
"themeToggle": "切換主題",
|
||||
"workflowTab": {
|
||||
|
||||
@@ -1,43 +1,10 @@
|
||||
{
|
||||
"Comfy-Desktop_CheckForUpdates": {
|
||||
"label": "检查更新"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "打开自定义节点文件夹"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenInputsFolder": {
|
||||
"label": "打开输入文件夹"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenLogsFolder": {
|
||||
"label": "打开日志文件夹"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelConfig": {
|
||||
"label": "打开 extra_model_paths.yaml"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelsFolder": {
|
||||
"label": "打开模型文件夹"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenOutputsFolder": {
|
||||
"label": "打开输出文件夹"
|
||||
},
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "打开开发者工具"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "桌面用户指南"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "退出"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "重新安装"
|
||||
},
|
||||
"Comfy-Desktop_Restart": {
|
||||
"label": "重启"
|
||||
},
|
||||
"Comfy_3DViewer_Open3DViewer": {
|
||||
"label": "为所选节点开启 3D 浏览器(Beta 版)"
|
||||
},
|
||||
"Comfy_BrowseModelAssets": {
|
||||
"label": "实验性:浏览模型资源"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "浏览模板"
|
||||
},
|
||||
@@ -125,6 +92,9 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "将选区转换为子图"
|
||||
},
|
||||
"Comfy_Graph_EditSubgraphWidgets": {
|
||||
"label": "编辑子图组件"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "退出子图"
|
||||
},
|
||||
@@ -134,6 +104,9 @@
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "添加框到选中节点"
|
||||
},
|
||||
"Comfy_Graph_ToggleWidgetPromotion": {
|
||||
"label": "切换悬停小部件的推广"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "解开所选子图"
|
||||
},
|
||||
@@ -239,6 +212,9 @@
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "显示设置对话框"
|
||||
},
|
||||
"Comfy_ToggleAssetAPI": {
|
||||
"label": "实验性:启用 AssetAPI"
|
||||
},
|
||||
"Comfy_ToggleCanvasInfo": {
|
||||
"label": "画布性能"
|
||||
},
|
||||
@@ -257,6 +233,9 @@
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "退出登录"
|
||||
},
|
||||
"Experimental_ToggleVueNodes": {
|
||||
"label": "实验性:启用 Vue 节点"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "关闭当前工作流"
|
||||
},
|
||||
@@ -290,6 +269,10 @@
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "切换焦点模式"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "切换资产侧边栏",
|
||||
"tooltip": "资产"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_model-library": {
|
||||
"label": "切换模型库侧边栏",
|
||||
"tooltip": "模型库"
|
||||
@@ -298,31 +281,8 @@
|
||||
"label": "切换节点库侧边栏",
|
||||
"tooltip": "节点库"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_queue": {
|
||||
"label": "切换执行队列侧边栏",
|
||||
"tooltip": "执行队列"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_workflows": {
|
||||
"label": "切换工作流侧边栏",
|
||||
"tooltip": "工作流"
|
||||
},
|
||||
"Comfy_BrowseModelAssets": {
|
||||
"label": "实验性:浏览模型资源"
|
||||
},
|
||||
"Comfy_Graph_EditSubgraphWidgets": {
|
||||
"label": "编辑子图组件"
|
||||
},
|
||||
"Comfy_Graph_ToggleWidgetPromotion": {
|
||||
"label": "切换悬停小部件的推广"
|
||||
},
|
||||
"Comfy_ToggleAssetAPI": {
|
||||
"label": "实验性:启用 AssetAPI"
|
||||
},
|
||||
"Experimental_ToggleVueNodes": {
|
||||
"label": "实验性:启用 Vue 节点"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "切换资产侧边栏",
|
||||
"tooltip": "资产"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1210,7 +1210,6 @@
|
||||
"Pin/Unpin Selected Nodes": "固定/取消固定选定节点",
|
||||
"Previous Opened Workflow": "上一个打开的工作流",
|
||||
"Publish": "发布",
|
||||
"Queue Panel": "队列面板",
|
||||
"Queue Prompt": "执行提示词",
|
||||
"Queue Prompt (Front)": "执行提示词 (优先执行)",
|
||||
"Queue Selected Output Nodes": "将所选输出节点加入队列",
|
||||
@@ -1670,18 +1669,6 @@
|
||||
},
|
||||
"openWorkflow": "在本地文件系统中打开工作流",
|
||||
"queue": "队列",
|
||||
"queueTab": {
|
||||
"backToAllTasks": "返回",
|
||||
"clearPendingTasks": "清除待处理任务",
|
||||
"containImagePreview": "填充图像预览",
|
||||
"coverImagePreview": "适应图像预览",
|
||||
"filter": "过滤输出",
|
||||
"filters": {
|
||||
"hideCached": "隐藏缓存",
|
||||
"hideCanceled": "隐藏已取消"
|
||||
},
|
||||
"showFlatList": "平铺结果"
|
||||
},
|
||||
"templates": "模板",
|
||||
"themeToggle": "切换主题",
|
||||
"workflowTab": {
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
:on-click="handleUploadClick"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--package-plus]" />
|
||||
<i class="icon-[lucide--upload]" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</div>
|
||||
|
||||
@@ -107,17 +107,10 @@ const {
|
||||
|
||||
const authActions = useFirebaseAuthActions()
|
||||
|
||||
// Get max sortOrder from settings in a group
|
||||
const getGroupSortOrder = (group: SettingTreeNode): number =>
|
||||
Math.max(0, ...flattenTree<SettingParams>(group).map((s) => s.sortOrder ?? 0))
|
||||
|
||||
// Sort groups for a category
|
||||
const sortedGroups = (category: SettingTreeNode): ISettingGroup[] => {
|
||||
return [...(category.children ?? [])]
|
||||
.sort((a, b) => {
|
||||
const orderDiff = getGroupSortOrder(b) - getGroupSortOrder(a)
|
||||
return orderDiff !== 0 ? orderDiff : a.label.localeCompare(b.label)
|
||||
})
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.map((group) => ({
|
||||
label: group.label,
|
||||
settings: flattenTree<SettingParams>(group).sort((a, b) => {
|
||||
|
||||
@@ -1082,28 +1082,24 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
|
||||
/**
|
||||
* Nodes 2.0 Settings
|
||||
* Vue Node System Settings
|
||||
*/
|
||||
{
|
||||
id: 'Comfy.VueNodes.Enabled',
|
||||
category: ['Comfy', 'Nodes 2.0', 'VueNodesEnabled'],
|
||||
name: 'Modern Node Design (Nodes 2.0)',
|
||||
name: 'Modern Node Design (Vue Nodes)',
|
||||
type: 'boolean',
|
||||
tooltip:
|
||||
'Modern: DOM-based rendering with enhanced interactivity, native browser features, and updated visual design. Classic: Traditional canvas rendering.',
|
||||
defaultValue: false,
|
||||
sortOrder: 100,
|
||||
experimental: true,
|
||||
versionAdded: '1.27.1'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.VueNodes.AutoScaleLayout',
|
||||
category: ['Comfy', 'Nodes 2.0', 'AutoScaleLayout'],
|
||||
name: 'Auto-scale layout (Nodes 2.0)',
|
||||
name: 'Auto-scale layout (Vue nodes)',
|
||||
tooltip:
|
||||
'Automatically scale node positions when switching to Vue rendering to prevent overlap',
|
||||
type: 'boolean',
|
||||
sortOrder: 50,
|
||||
experimental: true,
|
||||
defaultValue: true,
|
||||
versionAdded: '1.30.3'
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
ComfyWorkflowJSON,
|
||||
NodeId
|
||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useWorkflowThumbnail } from '@/renderer/core/thumbnail/useWorkflowThumbnail'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
@@ -329,6 +330,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
tabActivationHistory.value.shift()
|
||||
}
|
||||
|
||||
useCanvasStore().linearMode = !!loadedWorkflow.activeState.extra?.linearMode
|
||||
return loadedWorkflow
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||
// Reactive scale percentage that syncs with app.canvas.ds.scale
|
||||
const appScalePercentage = ref(100)
|
||||
|
||||
const linearMode = ref(false)
|
||||
|
||||
// Set up scale synchronization when canvas is available
|
||||
let originalOnChanged: ((scale: number, offset: Point) => void) | undefined =
|
||||
undefined
|
||||
@@ -138,6 +140,7 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||
groupSelected,
|
||||
rerouteSelected,
|
||||
appScalePercentage,
|
||||
linearMode,
|
||||
updateSelectedItems,
|
||||
getCanvas,
|
||||
setAppZoomFromPercentage,
|
||||
|
||||
31
src/renderer/core/layout/injectionKeys.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { InjectionKey } from 'vue'
|
||||
|
||||
import type { useTransformState } from '@/renderer/core/layout/transform/useTransformState'
|
||||
|
||||
/**
|
||||
* Lightweight, injectable transform state used by layout-aware components.
|
||||
*
|
||||
* Consumers use this interface to convert coordinates between LiteGraph's
|
||||
* canvas space and the DOM's screen space, access the current pan/zoom
|
||||
* (camera), and perform basic viewport culling checks.
|
||||
*
|
||||
* Coordinate mapping:
|
||||
* - screen = (canvas + offset) * scale
|
||||
* - canvas = screen / scale - offset
|
||||
*
|
||||
* The full implementation and additional helpers live in
|
||||
* `useTransformState()`. This interface deliberately exposes only the
|
||||
* minimal surface needed outside that composable.
|
||||
*
|
||||
* @example
|
||||
* const state = inject(TransformStateKey)!
|
||||
* const screen = state.canvasToScreen({ x: 100, y: 50 })
|
||||
*/
|
||||
export interface TransformState
|
||||
extends Pick<
|
||||
ReturnType<typeof useTransformState>,
|
||||
'screenToCanvas' | 'canvasToScreen' | 'camera' | 'isNodeInViewport'
|
||||
> {}
|
||||
|
||||
export const TransformStateKey: InjectionKey<TransformState> =
|
||||
Symbol('transformState')
|
||||
@@ -17,9 +17,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRafFn } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
import { computed, provide } from 'vue'
|
||||
|
||||
import type { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
import { TransformStateKey } from '@/renderer/core/layout/injectionKeys'
|
||||
import { useTransformSettling } from '@/renderer/core/layout/transform/useTransformSettling'
|
||||
import { useTransformState } from '@/renderer/core/layout/transform/useTransformState'
|
||||
import { useLOD } from '@/renderer/extensions/vueNodes/lod/useLOD'
|
||||
@@ -31,7 +32,14 @@ interface TransformPaneProps {
|
||||
|
||||
const props = defineProps<TransformPaneProps>()
|
||||
|
||||
const { camera, transformStyle, syncWithCanvas } = useTransformState()
|
||||
const {
|
||||
camera,
|
||||
transformStyle,
|
||||
syncWithCanvas,
|
||||
canvasToScreen,
|
||||
screenToCanvas,
|
||||
isNodeInViewport
|
||||
} = useTransformState()
|
||||
|
||||
const { isLOD } = useLOD(camera)
|
||||
|
||||
@@ -40,6 +48,13 @@ const { isTransforming: isInteracting } = useTransformSettling(canvasElement, {
|
||||
settleDelay: 512
|
||||
})
|
||||
|
||||
provide(TransformStateKey, {
|
||||
camera,
|
||||
canvasToScreen,
|
||||
screenToCanvas,
|
||||
isNodeInViewport
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
transformUpdate: []
|
||||
}>()
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
import { computed, reactive, readonly } from 'vue'
|
||||
|
||||
import type { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
import { createSharedComposable } from '@vueuse/core'
|
||||
|
||||
interface Point {
|
||||
x: number
|
||||
@@ -65,7 +64,7 @@ interface Camera {
|
||||
z: number // scale/zoom
|
||||
}
|
||||
|
||||
function useTransformStateIndividual() {
|
||||
export const useTransformState = () => {
|
||||
// Reactive state mirroring LiteGraph's canvas transform
|
||||
const camera = reactive<Camera>({
|
||||
x: 0,
|
||||
@@ -92,7 +91,7 @@ function useTransformStateIndividual() {
|
||||
*
|
||||
* @param canvas - LiteGraph canvas instance with DragAndScale (ds) transform state
|
||||
*/
|
||||
function syncWithCanvas(canvas: LGraphCanvas) {
|
||||
const syncWithCanvas = (canvas: LGraphCanvas) => {
|
||||
if (!canvas || !canvas.ds) return
|
||||
|
||||
// Mirror LiteGraph's transform state to Vue's reactive state
|
||||
@@ -113,7 +112,7 @@ function useTransformStateIndividual() {
|
||||
* @param point - Point in canvas coordinate system
|
||||
* @returns Point in screen coordinate system
|
||||
*/
|
||||
function canvasToScreen(point: Point): Point {
|
||||
const canvasToScreen = (point: Point): Point => {
|
||||
return {
|
||||
x: (point.x + camera.x) * camera.z,
|
||||
y: (point.y + camera.y) * camera.z
|
||||
@@ -139,10 +138,10 @@ function useTransformStateIndividual() {
|
||||
}
|
||||
|
||||
// Get node's screen bounds for culling
|
||||
function getNodeScreenBounds(
|
||||
pos: [number, number],
|
||||
size: [number, number]
|
||||
): DOMRect {
|
||||
const getNodeScreenBounds = (
|
||||
pos: ArrayLike<number>,
|
||||
size: ArrayLike<number>
|
||||
): DOMRect => {
|
||||
const topLeft = canvasToScreen({ x: pos[0], y: pos[1] })
|
||||
const width = size[0] * camera.z
|
||||
const height = size[1] * camera.z
|
||||
@@ -151,23 +150,23 @@ function useTransformStateIndividual() {
|
||||
}
|
||||
|
||||
// Helper: Calculate zoom-adjusted margin for viewport culling
|
||||
function calculateAdjustedMargin(baseMargin: number): number {
|
||||
const calculateAdjustedMargin = (baseMargin: number): number => {
|
||||
if (camera.z < 0.1) return Math.min(baseMargin * 5, 2.0)
|
||||
if (camera.z > 3.0) return Math.max(baseMargin * 0.5, 0.05)
|
||||
return baseMargin
|
||||
}
|
||||
|
||||
// Helper: Check if node is too small to be visible at current zoom
|
||||
function isNodeTooSmall(nodeSize: [number, number]): boolean {
|
||||
const isNodeTooSmall = (nodeSize: ArrayLike<number>): boolean => {
|
||||
const nodeScreenSize = Math.max(nodeSize[0], nodeSize[1]) * camera.z
|
||||
return nodeScreenSize < 4
|
||||
}
|
||||
|
||||
// Helper: Calculate expanded viewport bounds with margin
|
||||
function getExpandedViewportBounds(
|
||||
const getExpandedViewportBounds = (
|
||||
viewport: { width: number; height: number },
|
||||
margin: number
|
||||
) {
|
||||
) => {
|
||||
const marginX = viewport.width * margin
|
||||
const marginY = viewport.height * margin
|
||||
return {
|
||||
@@ -179,11 +178,11 @@ function useTransformStateIndividual() {
|
||||
}
|
||||
|
||||
// Helper: Test if node intersects with viewport bounds
|
||||
function testViewportIntersection(
|
||||
const testViewportIntersection = (
|
||||
screenPos: { x: number; y: number },
|
||||
nodeSize: [number, number],
|
||||
nodeSize: ArrayLike<number>,
|
||||
bounds: { left: number; right: number; top: number; bottom: number }
|
||||
): boolean {
|
||||
): boolean => {
|
||||
const nodeRight = screenPos.x + nodeSize[0] * camera.z
|
||||
const nodeBottom = screenPos.y + nodeSize[1] * camera.z
|
||||
|
||||
@@ -196,12 +195,12 @@ function useTransformStateIndividual() {
|
||||
}
|
||||
|
||||
// Check if node is within viewport with frustum and size-based culling
|
||||
function isNodeInViewport(
|
||||
nodePos: [number, number],
|
||||
nodeSize: [number, number],
|
||||
const isNodeInViewport = (
|
||||
nodePos: ArrayLike<number>,
|
||||
nodeSize: ArrayLike<number>,
|
||||
viewport: { width: number; height: number },
|
||||
margin: number = 0.2
|
||||
): boolean {
|
||||
): boolean => {
|
||||
// Early exit for tiny nodes
|
||||
if (isNodeTooSmall(nodeSize)) return false
|
||||
|
||||
@@ -213,10 +212,10 @@ function useTransformStateIndividual() {
|
||||
}
|
||||
|
||||
// Get viewport bounds in canvas coordinates (for spatial index queries)
|
||||
function getViewportBounds(
|
||||
const getViewportBounds = (
|
||||
viewport: { width: number; height: number },
|
||||
margin: number = 0.2
|
||||
) {
|
||||
) => {
|
||||
const marginX = viewport.width * margin
|
||||
const marginY = viewport.height * margin
|
||||
|
||||
@@ -245,7 +244,3 @@ function useTransformStateIndividual() {
|
||||
getViewportBounds
|
||||
}
|
||||
}
|
||||
|
||||
export const useTransformState = createSharedComposable(
|
||||
useTransformStateIndividual
|
||||
)
|
||||
|
||||
@@ -11,9 +11,9 @@ interface SpatialBounds {
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface PositionedNode {
|
||||
pos: [number, number]
|
||||
size: [number, number]
|
||||
interface PositionedNode {
|
||||
pos: ArrayLike<number>
|
||||
size: ArrayLike<number>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { calculateNodeBounds } from '@/renderer/core/spatial/boundsCalculator'
|
||||
import type { PositionedNode } from '@/renderer/core/spatial/boundsCalculator'
|
||||
|
||||
import type {
|
||||
IMinimapDataSource,
|
||||
@@ -30,12 +29,10 @@ export abstract class AbstractMinimapDataSource implements IMinimapDataSource {
|
||||
}
|
||||
|
||||
// Convert MinimapNodeData to the format expected by calculateNodeBounds
|
||||
const compatibleNodes = nodes.map(
|
||||
(node): PositionedNode => ({
|
||||
pos: [node.x, node.y],
|
||||
size: [node.width, node.height]
|
||||
})
|
||||
)
|
||||
const compatibleNodes = nodes.map((node) => ({
|
||||
pos: [node.x, node.y],
|
||||
size: [node.width, node.height]
|
||||
}))
|
||||
|
||||
const bounds = calculateNodeBounds(compatibleNodes)
|
||||
if (!bounds) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<SlotConnectionDot
|
||||
ref="connectionDotRef"
|
||||
:color="slotColor"
|
||||
:class="cn('-translate-x-1/2 w-3', errorClassesDot)"
|
||||
:class="cn('-translate-x-1/2', 'w-3', errorClassesDot)"
|
||||
@pointerdown="onPointerDown"
|
||||
/>
|
||||
|
||||
@@ -48,7 +48,6 @@ interface InputSlotProps {
|
||||
connected?: boolean
|
||||
compatible?: boolean
|
||||
dotOnly?: boolean
|
||||
socketless?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<InputSlotProps>()
|
||||
@@ -122,8 +121,7 @@ const slotWrapperClass = computed(() =>
|
||||
'lg-slot--connected': props.connected,
|
||||
'lg-slot--compatible': props.compatible,
|
||||
'opacity-40': shouldDim.value
|
||||
},
|
||||
props.socketless && 'pointer-events-none invisible'
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -19,12 +19,12 @@
|
||||
'outline-transparent outline-2',
|
||||
borderClass,
|
||||
outlineClass,
|
||||
cursorClass,
|
||||
{
|
||||
'before:rounded-2xl before:pointer-events-none before:absolute before:bg-bypass/60 before:inset-0':
|
||||
bypassed,
|
||||
'before:rounded-2xl before:pointer-events-none before:absolute before:inset-0':
|
||||
muted,
|
||||
'will-change-transform': isDragging,
|
||||
'ring-4 ring-primary-500 bg-primary-500/10': isDraggingOver
|
||||
},
|
||||
|
||||
@@ -39,10 +39,10 @@
|
||||
zIndex: zIndex,
|
||||
opacity: nodeOpacity,
|
||||
'--component-node-background': nodeBodyBackgroundColor
|
||||
}
|
||||
},
|
||||
dragStyle
|
||||
]"
|
||||
v-bind="remainingPointerHandlers"
|
||||
@pointerdown="nodeOnPointerdown"
|
||||
v-bind="pointerHandlers"
|
||||
@wheel="handleWheel"
|
||||
@contextmenu="handleContextMenu"
|
||||
@dragover.prevent="handleDragOver"
|
||||
@@ -137,31 +137,24 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, nextTick, onErrorCaptured, onMounted, ref, watch } from 'vue'
|
||||
import { computed, inject, onErrorCaptured, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { toggleNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { st } from '@/i18n'
|
||||
import {
|
||||
LGraphCanvas,
|
||||
LGraphEventMode,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphEventMode, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { useTransformState } from '@/renderer/core/layout/transform/useTransformState'
|
||||
import { TransformStateKey } from '@/renderer/core/layout/injectionKeys'
|
||||
import SlotConnectionDot from '@/renderer/extensions/vueNodes/components/SlotConnectionDot.vue'
|
||||
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
|
||||
import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions'
|
||||
import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex'
|
||||
import { useVueElementTracking } from '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking'
|
||||
import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState'
|
||||
import { useNodeDrag } from '@/renderer/extensions/vueNodes/layout/useNodeDrag'
|
||||
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
|
||||
import { useNodePreviewState } from '@/renderer/extensions/vueNodes/preview/useNodePreviewState'
|
||||
import { nonWidgetedInputs } from '@/renderer/extensions/vueNodes/utils/nodeDataUtils'
|
||||
@@ -195,13 +188,16 @@ const { nodeData, error = null } = defineProps<LGraphNodeProps>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { handleNodeCollapse, handleNodeTitleUpdate, handleNodeRightClick } =
|
||||
useNodeEventHandlers()
|
||||
const { bringNodeToFront } = useNodeZIndex()
|
||||
const {
|
||||
handleNodeCollapse,
|
||||
handleNodeTitleUpdate,
|
||||
handleNodeSelect,
|
||||
handleNodeRightClick
|
||||
} = useNodeEventHandlers()
|
||||
|
||||
useVueElementTracking(() => nodeData.id, 'node')
|
||||
|
||||
const transformState = useTransformState()
|
||||
const transformState = inject(TransformStateKey)
|
||||
if (!transformState) {
|
||||
throw new Error(
|
||||
'TransformState must be provided for node resize functionality'
|
||||
@@ -276,24 +272,10 @@ onErrorCaptured((error) => {
|
||||
})
|
||||
|
||||
const { position, size, zIndex, moveNodeTo } = useNodeLayout(() => nodeData.id)
|
||||
const { pointerHandlers } = useNodePointerInteractions(() => nodeData.id)
|
||||
const { onPointerdown, ...remainingPointerHandlers } = pointerHandlers
|
||||
const { startDrag } = useNodeDrag()
|
||||
|
||||
async function nodeOnPointerdown(event: PointerEvent) {
|
||||
if (event.altKey && lgraphNode.value) {
|
||||
const result = LGraphCanvas.cloneNodes([lgraphNode.value])
|
||||
if (result?.created?.length) {
|
||||
const [newNode] = result.created
|
||||
startDrag(event, `${newNode.id}`)
|
||||
layoutStore.isDraggingVueNodes.value = true
|
||||
await nextTick()
|
||||
bringNodeToFront(`${newNode.id}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
onPointerdown(event)
|
||||
}
|
||||
const { pointerHandlers, isDragging, dragStyle } = useNodePointerInteractions(
|
||||
() => nodeData,
|
||||
handleNodeSelect
|
||||
)
|
||||
|
||||
// Handle right-click context menu
|
||||
const handleContextMenu = (event: MouseEvent) => {
|
||||
@@ -301,7 +283,7 @@ const handleContextMenu = (event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
|
||||
// First handle the standard right-click behavior (selection)
|
||||
handleNodeRightClick(event as PointerEvent, nodeData.id)
|
||||
handleNodeRightClick(event as PointerEvent, nodeData)
|
||||
|
||||
// Show the node options menu at the cursor position
|
||||
const targetElement = event.currentTarget as HTMLElement
|
||||
@@ -440,16 +422,6 @@ const outlineClass = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const cursorClass = computed(() => {
|
||||
return cn(
|
||||
nodeData.flags?.pinned
|
||||
? 'cursor-default'
|
||||
: layoutStore.isDraggingVueNodes.value
|
||||
? 'cursor-grabbing'
|
||||
: 'cursor-grab'
|
||||
)
|
||||
})
|
||||
|
||||
// Event handlers
|
||||
const handleCollapse = () => {
|
||||
handleNodeCollapse(nodeData.id, !isCollapsed.value)
|
||||
|
||||