mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-17 17:28:58 +00:00
Compare commits
14 Commits
v1.41.11
...
feat/batch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc424f3d29 | ||
|
|
75022f302b | ||
|
|
959f1365dd | ||
|
|
216c707c87 | ||
|
|
47a2f312c8 | ||
|
|
80a4b31da4 | ||
|
|
93bb2d0d6f | ||
|
|
f04809d124 | ||
|
|
6171bd9ac4 | ||
|
|
218cf60f5f | ||
|
|
fc56f7ee85 | ||
|
|
c664b5bc38 | ||
|
|
724d60822a | ||
|
|
a15d3ce49b |
@@ -35,7 +35,7 @@
|
||||
}
|
||||
],
|
||||
"no-control-regex": "off",
|
||||
"no-eval": "error",
|
||||
"no-eval": "off",
|
||||
"no-redeclare": "error",
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
|
||||
@@ -4,6 +4,17 @@ import type { Page } from '@playwright/test'
|
||||
|
||||
import type { Position } from '../types'
|
||||
|
||||
function getFileType(fileName: string): string {
|
||||
if (fileName.endsWith('.png')) return 'image/png'
|
||||
if (fileName.endsWith('.svg')) return 'image/svg+xml'
|
||||
if (fileName.endsWith('.webp')) return 'image/webp'
|
||||
if (fileName.endsWith('.webm')) return 'video/webm'
|
||||
if (fileName.endsWith('.json')) return 'application/json'
|
||||
if (fileName.endsWith('.glb')) return 'model/gltf-binary'
|
||||
if (fileName.endsWith('.avif')) return 'image/avif'
|
||||
return 'application/octet-stream'
|
||||
}
|
||||
|
||||
export class DragDropHelper {
|
||||
constructor(
|
||||
private readonly page: Page,
|
||||
@@ -48,17 +59,6 @@ export class DragDropHelper {
|
||||
const filePath = this.assetPath(fileName)
|
||||
const buffer = readFileSync(filePath)
|
||||
|
||||
const getFileType = (fileName: string) => {
|
||||
if (fileName.endsWith('.png')) return 'image/png'
|
||||
if (fileName.endsWith('.svg')) return 'image/svg+xml'
|
||||
if (fileName.endsWith('.webp')) return 'image/webp'
|
||||
if (fileName.endsWith('.webm')) return 'video/webm'
|
||||
if (fileName.endsWith('.json')) return 'application/json'
|
||||
if (fileName.endsWith('.glb')) return 'model/gltf-binary'
|
||||
if (fileName.endsWith('.avif')) return 'image/avif'
|
||||
return 'application/octet-stream'
|
||||
}
|
||||
|
||||
evaluateParams.fileName = fileName
|
||||
evaluateParams.fileType = getFileType(fileName)
|
||||
evaluateParams.buffer = [...new Uint8Array(buffer)]
|
||||
@@ -155,6 +155,104 @@ export class DragDropHelper {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async dragAndDropFiles(
|
||||
fileNames: string[],
|
||||
options: {
|
||||
dropPosition?: Position
|
||||
waitForUploadCount?: number
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
const { dropPosition = { x: 100, y: 100 }, waitForUploadCount = 0 } =
|
||||
options
|
||||
|
||||
const files = fileNames.map((fileName) => {
|
||||
const filePath = this.assetPath(fileName)
|
||||
const buffer = readFileSync(filePath)
|
||||
|
||||
return {
|
||||
fileName,
|
||||
fileType: getFileType(fileName),
|
||||
buffer: [...new Uint8Array(buffer)]
|
||||
}
|
||||
})
|
||||
|
||||
let uploadResponsePromise: Promise<unknown> | null = null
|
||||
if (waitForUploadCount > 0) {
|
||||
let uploadCount = 0
|
||||
uploadResponsePromise = new Promise<void>((resolve) => {
|
||||
const handler = (resp: { url(): string; status(): number }) => {
|
||||
if (resp.url().includes('/upload/') && resp.status() === 200) {
|
||||
uploadCount++
|
||||
if (uploadCount >= waitForUploadCount) {
|
||||
this.page.off('response', handler)
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
}
|
||||
this.page.on('response', handler)
|
||||
})
|
||||
}
|
||||
|
||||
await this.page.evaluate(
|
||||
async (params) => {
|
||||
const dataTransfer = new DataTransfer()
|
||||
|
||||
for (const f of params.files) {
|
||||
const file = new File([new Uint8Array(f.buffer)], f.fileName, {
|
||||
type: f.fileType
|
||||
})
|
||||
dataTransfer.items.add(file)
|
||||
}
|
||||
|
||||
const targetElement = document.elementFromPoint(
|
||||
params.dropPosition.x,
|
||||
params.dropPosition.y
|
||||
)
|
||||
|
||||
if (!targetElement) {
|
||||
throw new Error(
|
||||
`No element found at drop position: (${params.dropPosition.x}, ${params.dropPosition.y}).`
|
||||
)
|
||||
}
|
||||
|
||||
const eventOptions = {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
dataTransfer,
|
||||
clientX: params.dropPosition.x,
|
||||
clientY: params.dropPosition.y
|
||||
}
|
||||
|
||||
const graphCanvasElement = document.querySelector('#graph-canvas')
|
||||
if (graphCanvasElement && !graphCanvasElement.contains(targetElement)) {
|
||||
graphCanvasElement.dispatchEvent(
|
||||
new DragEvent('dragover', eventOptions)
|
||||
)
|
||||
}
|
||||
|
||||
const dropEvent = new DragEvent('drop', eventOptions)
|
||||
Object.defineProperty(dropEvent, 'preventDefault', {
|
||||
value: () => {},
|
||||
writable: false
|
||||
})
|
||||
Object.defineProperty(dropEvent, 'stopPropagation', {
|
||||
value: () => {},
|
||||
writable: false
|
||||
})
|
||||
|
||||
targetElement.dispatchEvent(new DragEvent('dragover', eventOptions))
|
||||
targetElement.dispatchEvent(dropEvent)
|
||||
},
|
||||
{ files, dropPosition }
|
||||
)
|
||||
|
||||
if (uploadResponsePromise) {
|
||||
await uploadResponsePromise
|
||||
}
|
||||
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async dragAndDropFile(
|
||||
fileName: string,
|
||||
options: { dropPosition?: Position; waitForUpload?: boolean } = {}
|
||||
|
||||
90
browser_tests/tests/batchImageImport.spec.ts
Normal file
90
browser_tests/tests/batchImageImport.spec.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import type { WorkspaceStore } from '../types/globals'
|
||||
|
||||
test.describe('Batch Image Import', () => {
|
||||
test('Dropping multiple images creates LoadImage nodes and a BatchImagesNode', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
|
||||
|
||||
await comfyPage.dragDrop.dragAndDropFiles(
|
||||
['image32x32.webp', 'image64x64.webp'],
|
||||
{ waitForUploadCount: 2 }
|
||||
)
|
||||
|
||||
await expect
|
||||
.poll(() => comfyPage.nodeOps.getGraphNodesCount(), { timeout: 10000 })
|
||||
.toBe(initialCount + 3)
|
||||
|
||||
const batchNodes =
|
||||
await comfyPage.nodeOps.getNodeRefsByType('BatchImagesNode')
|
||||
expect(batchNodes).toHaveLength(1)
|
||||
})
|
||||
|
||||
test('Dropping a single image does not create a BatchImagesNode', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
|
||||
|
||||
await comfyPage.dragDrop.dragAndDropFile('image32x32.webp', {
|
||||
waitForUpload: true
|
||||
})
|
||||
|
||||
await expect
|
||||
.poll(() => comfyPage.nodeOps.getGraphNodesCount(), { timeout: 10000 })
|
||||
.toBe(initialCount + 1)
|
||||
|
||||
const batchNodes =
|
||||
await comfyPage.nodeOps.getNodeRefsByType('BatchImagesNode')
|
||||
expect(batchNodes).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('Batch image import produces a single undo entry', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
|
||||
const initialUndoSize = await comfyPage.workflow.getUndoQueueSize()
|
||||
|
||||
await comfyPage.dragDrop.dragAndDropFiles(
|
||||
['image32x32.webp', 'image64x64.webp'],
|
||||
{ waitForUploadCount: 2 }
|
||||
)
|
||||
|
||||
await expect
|
||||
.poll(() => comfyPage.nodeOps.getGraphNodesCount(), { timeout: 10000 })
|
||||
.toBe(initialCount + 3)
|
||||
|
||||
await expect
|
||||
.poll(() => comfyPage.workflow.getUndoQueueSize(), { timeout: 5000 })
|
||||
.toBe((initialUndoSize ?? 0) + 1)
|
||||
})
|
||||
|
||||
test('Batch image import can be undone as a single action', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
|
||||
|
||||
await comfyPage.dragDrop.dragAndDropFiles(
|
||||
['image32x32.webp', 'image64x64.webp'],
|
||||
{ waitForUploadCount: 2 }
|
||||
)
|
||||
|
||||
await expect
|
||||
.poll(() => comfyPage.nodeOps.getGraphNodesCount(), { timeout: 10000 })
|
||||
.toBe(initialCount + 3)
|
||||
|
||||
// Call undo directly on the change tracker to avoid keyboard focus issues
|
||||
await comfyPage.page.evaluate(async () => {
|
||||
const workflow = (window.app!.extensionManager as WorkspaceStore).workflow
|
||||
.activeWorkflow
|
||||
await workflow?.changeTracker.undo()
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect
|
||||
.poll(() => comfyPage.nodeOps.getGraphNodesCount(), { timeout: 10000 })
|
||||
.toBe(initialCount)
|
||||
})
|
||||
})
|
||||
63
browser_tests/tests/batchImageNodePositioning.spec.ts
Normal file
63
browser_tests/tests/batchImageNodePositioning.spec.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Batch Image Node Positioning', () => {
|
||||
test('Dropped image nodes should not overlap', async ({ comfyPage }) => {
|
||||
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
|
||||
|
||||
await comfyPage.dragDrop.dragAndDropFiles(
|
||||
['image32x32.webp', 'image64x64.webp'],
|
||||
{ waitForUploadCount: 2 }
|
||||
)
|
||||
|
||||
await expect
|
||||
.poll(() => comfyPage.nodeOps.getGraphNodesCount(), { timeout: 10000 })
|
||||
.toBe(initialCount + 3)
|
||||
|
||||
const loadImageNodes =
|
||||
await comfyPage.nodeOps.getNodeRefsByType('LoadImage')
|
||||
expect(loadImageNodes).toHaveLength(2)
|
||||
|
||||
// Get bounding boxes via page.evaluate to read all node bounds atomically
|
||||
const boundingBoxes = await comfyPage.page.evaluate(() => {
|
||||
const graph = window.app!.graph
|
||||
const loadImageNodes = graph.nodes.filter((n) => n.type === 'LoadImage')
|
||||
return loadImageNodes.map((node) => {
|
||||
const [x, y, w, h] = node.getBounding()
|
||||
return { x, y, width: w, height: h }
|
||||
})
|
||||
})
|
||||
|
||||
expect(boundingBoxes).toHaveLength(2)
|
||||
|
||||
// Assert: no two bounding boxes overlap
|
||||
for (let i = 0; i < boundingBoxes.length; i++) {
|
||||
for (let j = i + 1; j < boundingBoxes.length; j++) {
|
||||
const a = boundingBoxes[i]
|
||||
const b = boundingBoxes[j]
|
||||
|
||||
const overlapsX = a.x < b.x + b.width && a.x + a.width > b.x
|
||||
const overlapsY = a.y < b.y + b.height && a.y + a.height > b.y
|
||||
const overlaps = overlapsX && overlapsY
|
||||
|
||||
expect(
|
||||
overlaps,
|
||||
`Nodes ${i} and ${j} overlap! ` +
|
||||
`Node ${i}: (${a.x}, ${a.y}, ${a.width}x${a.height}), ` +
|
||||
`Node ${j}: (${b.x}, ${b.y}, ${b.width}x${b.height})`
|
||||
).toBe(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify minimum spacing between consecutive nodes (sorted by Y position)
|
||||
const sorted = [...boundingBoxes].sort((a, b) => a.y - b.y)
|
||||
for (let i = 0; i < sorted.length - 1; i++) {
|
||||
const gap = sorted[i + 1].y - (sorted[i].y + sorted[i].height)
|
||||
expect(
|
||||
gap,
|
||||
`Gap between nodes ${i} and ${i + 1} is ${gap}px, expected >= 10px`
|
||||
).toBeGreaterThanOrEqual(10)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -67,66 +67,4 @@ test.describe('Performance', { tag: ['@perf'] }, () => {
|
||||
recordMeasurement(m)
|
||||
console.log(`Clipping: ${m.layouts} forced layouts`)
|
||||
})
|
||||
|
||||
test('subgraph idle style recalculations', async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow('subgraphs/nested-subgraph')
|
||||
await comfyPage.perf.startMeasuring()
|
||||
|
||||
for (let i = 0; i < 120; i++) {
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
const m = await comfyPage.perf.stopMeasuring('subgraph-idle')
|
||||
recordMeasurement(m)
|
||||
console.log(
|
||||
`Subgraph idle: ${m.styleRecalcs} style recalcs, ${m.layouts} layouts`
|
||||
)
|
||||
})
|
||||
|
||||
test('subgraph mouse interaction style recalculations', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('subgraphs/nested-subgraph')
|
||||
await comfyPage.perf.startMeasuring()
|
||||
|
||||
const canvas = comfyPage.canvas
|
||||
const box = await canvas.boundingBox()
|
||||
if (!box) throw new Error('Canvas bounding box not available')
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
await comfyPage.page.mouse.move(
|
||||
box.x + (box.width * i) / 100,
|
||||
box.y + (box.height * (i % 3)) / 3
|
||||
)
|
||||
}
|
||||
|
||||
const m = await comfyPage.perf.stopMeasuring('subgraph-mouse-sweep')
|
||||
recordMeasurement(m)
|
||||
console.log(
|
||||
`Subgraph mouse sweep: ${m.styleRecalcs} style recalcs, ${m.layouts} layouts`
|
||||
)
|
||||
})
|
||||
|
||||
test('subgraph DOM widget clipping during node selection', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('subgraphs/nested-subgraph')
|
||||
await comfyPage.perf.startMeasuring()
|
||||
|
||||
const canvas = comfyPage.canvas
|
||||
const box = await canvas.boundingBox()
|
||||
if (!box) throw new Error('Canvas bounding box not available')
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await comfyPage.page.mouse.click(
|
||||
box.x + box.width / 3 + (i % 5) * 30,
|
||||
box.y + box.height / 3 + (i % 4) * 30
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
const m = await comfyPage.perf.stopMeasuring('subgraph-dom-widget-clipping')
|
||||
recordMeasurement(m)
|
||||
console.log(`Subgraph clipping: ${m.layouts} forced layouts`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.41.11",
|
||||
"version": "1.41.10",
|
||||
"private": true,
|
||||
"description": "Official front-end implementation of ComfyUI",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -296,13 +296,11 @@ describe('TopMenuSection', () => {
|
||||
describe('inline progress summary', () => {
|
||||
const configureSettings = (
|
||||
pinia: ReturnType<typeof createTestingPinia>,
|
||||
qpoV2Enabled: boolean,
|
||||
showRunProgressBar = true
|
||||
qpoV2Enabled: boolean
|
||||
) => {
|
||||
const settingStore = useSettingStore(pinia)
|
||||
vi.mocked(settingStore.get).mockImplementation((key) => {
|
||||
if (key === 'Comfy.Queue.QPOV2') return qpoV2Enabled
|
||||
if (key === 'Comfy.Queue.ShowRunProgressBar') return showRunProgressBar
|
||||
if (key === 'Comfy.UseNewMenu') return 'Top'
|
||||
return undefined
|
||||
})
|
||||
@@ -334,19 +332,6 @@ describe('TopMenuSection', () => {
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('does not render inline progress summary when run progress bar is disabled', async () => {
|
||||
const pinia = createTestingPinia({ createSpy: vi.fn })
|
||||
configureSettings(pinia, true, false)
|
||||
|
||||
const wrapper = createWrapper({ pinia })
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'QueueInlineProgressSummary' }).exists()
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('teleports inline progress summary when actionbar is floating', async () => {
|
||||
localStorage.setItem('Comfy.MenuPosition.Docked', 'false')
|
||||
const actionbarTarget = document.createElement('div')
|
||||
|
||||
@@ -125,7 +125,6 @@ import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
|
||||
import LoginButton from '@/components/topbar/LoginButton.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
@@ -165,16 +164,14 @@ const isActionbarFloating = computed(
|
||||
const isIntegratedTabBar = computed(
|
||||
() => settingStore.get('Comfy.UI.TabBarLayout') === 'Integrated'
|
||||
)
|
||||
const { isQueuePanelV2Enabled, isRunProgressBarEnabled } =
|
||||
useQueueFeatureFlags()
|
||||
const isQueuePanelV2Enabled = computed(() =>
|
||||
settingStore.get('Comfy.Queue.QPOV2')
|
||||
)
|
||||
const isQueueProgressOverlayEnabled = computed(
|
||||
() => !isQueuePanelV2Enabled.value
|
||||
)
|
||||
const shouldShowInlineProgressSummary = computed(
|
||||
() =>
|
||||
isQueuePanelV2Enabled.value &&
|
||||
isActionbarEnabled.value &&
|
||||
isRunProgressBarEnabled.value
|
||||
() => isQueuePanelV2Enabled.value && isActionbarEnabled.value
|
||||
)
|
||||
const shouldShowQueueNotificationBanners = computed(
|
||||
() => isActionbarEnabled.value
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'
|
||||
import { i18n } from '@/i18n'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
const configureSettings = (
|
||||
pinia: ReturnType<typeof createTestingPinia>,
|
||||
showRunProgressBar: boolean
|
||||
) => {
|
||||
const settingStore = useSettingStore(pinia)
|
||||
vi.mocked(settingStore.get).mockImplementation((key) => {
|
||||
if (key === 'Comfy.UseNewMenu') return 'Top'
|
||||
if (key === 'Comfy.Queue.QPOV2') return true
|
||||
if (key === 'Comfy.Queue.ShowRunProgressBar') return showRunProgressBar
|
||||
return undefined
|
||||
})
|
||||
}
|
||||
|
||||
const mountActionbar = (showRunProgressBar: boolean) => {
|
||||
const topMenuContainer = document.createElement('div')
|
||||
document.body.appendChild(topMenuContainer)
|
||||
|
||||
const pinia = createTestingPinia({ createSpy: vi.fn })
|
||||
configureSettings(pinia, showRunProgressBar)
|
||||
|
||||
const wrapper = mount(ComfyActionbar, {
|
||||
attachTo: document.body,
|
||||
props: {
|
||||
topMenuContainer,
|
||||
queueOverlayExpanded: false
|
||||
},
|
||||
global: {
|
||||
plugins: [pinia, i18n],
|
||||
stubs: {
|
||||
ContextMenu: {
|
||||
name: 'ContextMenu',
|
||||
template: '<div />'
|
||||
},
|
||||
Panel: {
|
||||
name: 'Panel',
|
||||
template: '<div><slot /></div>'
|
||||
},
|
||||
StatusBadge: true,
|
||||
ComfyRunButton: {
|
||||
name: 'ComfyRunButton',
|
||||
template: '<button type="button">Run</button>'
|
||||
},
|
||||
QueueInlineProgress: true
|
||||
},
|
||||
directives: {
|
||||
tooltip: () => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
wrapper,
|
||||
topMenuContainer
|
||||
}
|
||||
}
|
||||
|
||||
describe('ComfyActionbar', () => {
|
||||
beforeEach(() => {
|
||||
i18n.global.locale.value = 'en'
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
it('teleports inline progress when run progress bar is enabled', async () => {
|
||||
const { wrapper, topMenuContainer } = mountActionbar(true)
|
||||
|
||||
try {
|
||||
await nextTick()
|
||||
|
||||
expect(
|
||||
topMenuContainer.querySelector('[data-testid="queue-inline-progress"]')
|
||||
).not.toBeNull()
|
||||
} finally {
|
||||
wrapper.unmount()
|
||||
topMenuContainer.remove()
|
||||
}
|
||||
})
|
||||
|
||||
it('does not teleport inline progress when run progress bar is disabled', async () => {
|
||||
const { wrapper, topMenuContainer } = mountActionbar(false)
|
||||
|
||||
try {
|
||||
await nextTick()
|
||||
|
||||
expect(
|
||||
topMenuContainer.querySelector('[data-testid="queue-inline-progress"]')
|
||||
).toBeNull()
|
||||
} finally {
|
||||
wrapper.unmount()
|
||||
topMenuContainer.remove()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -107,7 +107,6 @@ import { useI18n } from 'vue-i18n'
|
||||
import StatusBadge from '@/components/common/StatusBadge.vue'
|
||||
import QueueInlineProgress from '@/components/queue/QueueInlineProgress.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
@@ -128,7 +127,7 @@ const emit = defineEmits<{
|
||||
(event: 'update:progressTarget', target: HTMLElement | null): void
|
||||
}>()
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const settingsStore = useSettingStore()
|
||||
const commandStore = useCommandStore()
|
||||
const executionStore = useExecutionStore()
|
||||
const queueStore = useQueueStore()
|
||||
@@ -138,10 +137,11 @@ const { isIdle: isExecutionIdle } = storeToRefs(executionStore)
|
||||
const { activeJobsCount } = storeToRefs(queueStore)
|
||||
const { activeSidebarTabId } = storeToRefs(sidebarTabStore)
|
||||
|
||||
const position = computed(() => settingStore.get('Comfy.UseNewMenu'))
|
||||
const position = computed(() => settingsStore.get('Comfy.UseNewMenu'))
|
||||
const visible = computed(() => position.value !== 'Disabled')
|
||||
const { isQueuePanelV2Enabled, isRunProgressBarEnabled } =
|
||||
useQueueFeatureFlags()
|
||||
const isQueuePanelV2Enabled = computed(() =>
|
||||
settingsStore.get('Comfy.Queue.QPOV2')
|
||||
)
|
||||
|
||||
const panelRef = ref<ComponentPublicInstance | null>(null)
|
||||
const panelElement = computed<HTMLElement | null>(() => {
|
||||
@@ -325,13 +325,7 @@ const onMouseLeaveDropZone = () => {
|
||||
}
|
||||
|
||||
const inlineProgressTarget = computed(() => {
|
||||
if (
|
||||
!visible.value ||
|
||||
!isQueuePanelV2Enabled.value ||
|
||||
!isRunProgressBarEnabled.value
|
||||
) {
|
||||
return null
|
||||
}
|
||||
if (!visible.value || !isQueuePanelV2Enabled.value) return null
|
||||
if (isDocked.value) return topMenuContainer ?? null
|
||||
return panelElement.value
|
||||
})
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { statusBadgeVariants } from './statusBadge.variants'
|
||||
import type { StatusBadgeVariants } from './statusBadge.variants'
|
||||
|
||||
@@ -13,17 +11,17 @@ const {
|
||||
severity?: StatusBadgeVariants['severity']
|
||||
variant?: StatusBadgeVariants['variant']
|
||||
}>()
|
||||
|
||||
const badgeClass = computed(() =>
|
||||
statusBadgeVariants({
|
||||
severity,
|
||||
variant: variant ?? (label == null ? 'dot' : 'label')
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :class="badgeClass">
|
||||
<span
|
||||
:class="
|
||||
statusBadgeVariants({
|
||||
severity,
|
||||
variant: variant ?? (label == null ? 'dot' : 'label')
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ label }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { defineComponent, h } from 'vue'
|
||||
|
||||
import { i18n } from '@/i18n'
|
||||
import JobHistoryActionsMenu from '@/components/queue/JobHistoryActionsMenu.vue'
|
||||
|
||||
const popoverCloseSpy = vi.fn()
|
||||
|
||||
vi.mock('@/components/ui/Popover.vue', () => {
|
||||
const PopoverStub = defineComponent({
|
||||
name: 'Popover',
|
||||
setup(_, { slots }) {
|
||||
return () =>
|
||||
h('div', [
|
||||
slots.button?.(),
|
||||
slots.default?.({
|
||||
close: () => {
|
||||
popoverCloseSpy()
|
||||
}
|
||||
})
|
||||
])
|
||||
}
|
||||
})
|
||||
return { default: PopoverStub }
|
||||
})
|
||||
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
isCloud: false
|
||||
}))
|
||||
|
||||
const mockGetSetting = vi.fn<(key: string) => boolean | undefined>((key) =>
|
||||
key === 'Comfy.Queue.QPOV2' || key === 'Comfy.Queue.ShowRunProgressBar'
|
||||
? true
|
||||
: undefined
|
||||
)
|
||||
const mockSetSetting = vi.fn()
|
||||
const mockSetMany = vi.fn()
|
||||
const mockSidebarTabStore = {
|
||||
activeSidebarTabId: null as string | null
|
||||
}
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: () => ({
|
||||
get: mockGetSetting,
|
||||
set: mockSetSetting,
|
||||
setMany: mockSetMany
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/workspace/sidebarTabStore', () => ({
|
||||
useSidebarTabStore: () => mockSidebarTabStore
|
||||
}))
|
||||
|
||||
const mountMenu = () =>
|
||||
mount(JobHistoryActionsMenu, {
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
directives: { tooltip: () => {} }
|
||||
}
|
||||
})
|
||||
|
||||
describe('JobHistoryActionsMenu', () => {
|
||||
beforeEach(() => {
|
||||
i18n.global.locale.value = 'en'
|
||||
popoverCloseSpy.mockClear()
|
||||
mockSetSetting.mockClear()
|
||||
mockSetMany.mockClear()
|
||||
mockSidebarTabStore.activeSidebarTabId = null
|
||||
mockGetSetting.mockImplementation((key: string) =>
|
||||
key === 'Comfy.Queue.QPOV2' || key === 'Comfy.Queue.ShowRunProgressBar'
|
||||
? true
|
||||
: undefined
|
||||
)
|
||||
})
|
||||
|
||||
it('toggles show run progress bar setting from the menu', async () => {
|
||||
const wrapper = mountMenu()
|
||||
|
||||
const showRunProgressBarButton = wrapper.get(
|
||||
'[data-testid="show-run-progress-bar-action"]'
|
||||
)
|
||||
await showRunProgressBarButton.trigger('click')
|
||||
|
||||
expect(mockSetSetting).toHaveBeenCalledTimes(1)
|
||||
expect(mockSetSetting).toHaveBeenCalledWith(
|
||||
'Comfy.Queue.ShowRunProgressBar',
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it('opens docked job history sidebar when enabling from the menu', async () => {
|
||||
mockGetSetting.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Queue.QPOV2') return false
|
||||
if (key === 'Comfy.Queue.ShowRunProgressBar') return true
|
||||
return undefined
|
||||
})
|
||||
const wrapper = mountMenu()
|
||||
|
||||
const dockedJobHistoryButton = wrapper.get(
|
||||
'[data-testid="docked-job-history-action"]'
|
||||
)
|
||||
await dockedJobHistoryButton.trigger('click')
|
||||
|
||||
expect(popoverCloseSpy).toHaveBeenCalledTimes(1)
|
||||
expect(mockSetSetting).toHaveBeenCalledTimes(1)
|
||||
expect(mockSetSetting).toHaveBeenCalledWith('Comfy.Queue.QPOV2', true)
|
||||
expect(mockSetMany).not.toHaveBeenCalled()
|
||||
expect(mockSidebarTabStore.activeSidebarTabId).toBe('job-history')
|
||||
})
|
||||
|
||||
it('emits clear history from the menu', async () => {
|
||||
const wrapper = mountMenu()
|
||||
|
||||
const clearHistoryButton = wrapper.get(
|
||||
'[data-testid="clear-history-action"]'
|
||||
)
|
||||
await clearHistoryButton.trigger('click')
|
||||
|
||||
expect(popoverCloseSpy).toHaveBeenCalledTimes(1)
|
||||
expect(wrapper.emitted('clearHistory')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
@@ -19,7 +19,7 @@
|
||||
data-testid="docked-job-history-action"
|
||||
class="w-full justify-between text-sm font-light"
|
||||
variant="textonly"
|
||||
size="md"
|
||||
size="sm"
|
||||
@click="onToggleDockedJobHistory(close)"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
@@ -35,32 +35,14 @@
|
||||
class="icon-[lucide--check] size-4"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="show-run-progress-bar-action"
|
||||
class="w-full justify-between text-sm font-light"
|
||||
variant="textonly"
|
||||
size="md"
|
||||
@click="onToggleRunProgressBar"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<i class="icon-[lucide--hourglass] size-4 text-text-secondary" />
|
||||
<span>{{
|
||||
t('sideToolbar.queueProgressOverlay.showRunProgressBar')
|
||||
}}</span>
|
||||
</span>
|
||||
<i
|
||||
v-if="isRunProgressBarEnabled"
|
||||
class="icon-[lucide--check] size-4"
|
||||
/>
|
||||
</Button>
|
||||
<!-- TODO: Bug in assets sidebar panel derives assets from history, so despite this not deleting the assets, it still effectively shows to the user as deleted -->
|
||||
<template v-if="showClearHistoryAction">
|
||||
<div class="my-1 border-t border-interface-stroke" />
|
||||
<Button
|
||||
data-testid="clear-history-action"
|
||||
class="h-auto min-h-8 w-full items-start justify-start whitespace-normal"
|
||||
class="h-auto min-h-0 w-full items-start justify-start whitespace-normal"
|
||||
variant="textonly"
|
||||
size="md"
|
||||
size="sm"
|
||||
@click="onClearHistoryFromMenu(close)"
|
||||
>
|
||||
<i
|
||||
@@ -94,7 +76,6 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Popover from '@/components/ui/Popover.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
@@ -109,8 +90,9 @@ const settingStore = useSettingStore()
|
||||
const sidebarTabStore = useSidebarTabStore()
|
||||
|
||||
const moreTooltipConfig = computed(() => buildTooltipConfig(t('g.more')))
|
||||
const { isQueuePanelV2Enabled, isRunProgressBarEnabled } =
|
||||
useQueueFeatureFlags()
|
||||
const isQueuePanelV2Enabled = computed(() =>
|
||||
settingStore.get('Comfy.Queue.QPOV2')
|
||||
)
|
||||
const showClearHistoryAction = computed(() => !isCloud)
|
||||
|
||||
const onClearHistoryFromMenu = (close: () => void) => {
|
||||
@@ -136,11 +118,4 @@ const onToggleDockedJobHistory = async (close: () => void) => {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const onToggleRunProgressBar = async () => {
|
||||
await settingStore.set(
|
||||
'Comfy.Queue.ShowRunProgressBar',
|
||||
!isRunProgressBarEnabled.value
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { defineComponent, h } from 'vue'
|
||||
|
||||
import { i18n } from '@/i18n'
|
||||
|
||||
const popoverCloseSpy = vi.fn()
|
||||
|
||||
vi.mock('@/components/ui/Popover.vue', () => {
|
||||
@@ -25,9 +24,7 @@ vi.mock('@/components/ui/Popover.vue', () => {
|
||||
})
|
||||
|
||||
const mockGetSetting = vi.fn<(key: string) => boolean | undefined>((key) =>
|
||||
key === 'Comfy.Queue.QPOV2' || key === 'Comfy.Queue.ShowRunProgressBar'
|
||||
? true
|
||||
: undefined
|
||||
key === 'Comfy.Queue.QPOV2' ? true : undefined
|
||||
)
|
||||
const mockSetSetting = vi.fn()
|
||||
const mockSetMany = vi.fn()
|
||||
@@ -55,6 +52,27 @@ const tooltipDirectiveStub = {
|
||||
updated: vi.fn()
|
||||
}
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
g: { more: 'More' },
|
||||
sideToolbar: {
|
||||
queueProgressOverlay: {
|
||||
queuedSuffix: 'queued',
|
||||
clearQueued: 'Clear queued',
|
||||
clearQueueTooltip: 'Clear queue',
|
||||
clearAllJobsTooltip: 'Cancel all running jobs',
|
||||
moreOptions: 'More options',
|
||||
clearHistory: 'Clear history',
|
||||
dockedJobHistory: 'Docked Job History'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const mountHeader = (props = {}) =>
|
||||
mount(QueueOverlayHeader, {
|
||||
props: {
|
||||
@@ -70,7 +88,6 @@ const mountHeader = (props = {}) =>
|
||||
|
||||
describe('QueueOverlayHeader', () => {
|
||||
beforeEach(() => {
|
||||
i18n.global.locale.value = 'en'
|
||||
popoverCloseSpy.mockClear()
|
||||
mockSetSetting.mockClear()
|
||||
mockSetMany.mockClear()
|
||||
@@ -190,19 +207,4 @@ describe('QueueOverlayHeader', () => {
|
||||
'Comfy.Queue.History.Expanded': true
|
||||
})
|
||||
})
|
||||
|
||||
it('toggles show run progress bar setting from the menu', async () => {
|
||||
const wrapper = mountHeader()
|
||||
|
||||
const showRunProgressBarButton = wrapper.get(
|
||||
'[data-testid="show-run-progress-bar-action"]'
|
||||
)
|
||||
await showRunProgressBarButton.trigger('click')
|
||||
|
||||
expect(mockSetSetting).toHaveBeenCalledTimes(1)
|
||||
expect(mockSetSetting).toHaveBeenCalledWith(
|
||||
'Comfy.Queue.ShowRunProgressBar',
|
||||
false
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -323,90 +323,6 @@ describe('useErrorGroups', () => {
|
||||
)
|
||||
expect(promptGroup).toBeDefined()
|
||||
})
|
||||
|
||||
it('sorts cards within an execution group by nodeId numerically', async () => {
|
||||
const { store, groups } = createErrorGroups()
|
||||
store.lastNodeErrors = {
|
||||
'10': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
errors: [{ type: 'err', message: 'Error', details: '' }]
|
||||
},
|
||||
'2': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
errors: [{ type: 'err', message: 'Error', details: '' }]
|
||||
},
|
||||
'1': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
errors: [{ type: 'err', message: 'Error', details: '' }]
|
||||
}
|
||||
}
|
||||
await nextTick()
|
||||
|
||||
const execGroup = groups.allErrorGroups.value.find(
|
||||
(g) => g.type === 'execution'
|
||||
)
|
||||
const nodeIds = execGroup?.cards.map((c) => c.nodeId)
|
||||
expect(nodeIds).toEqual(['1', '2', '10'])
|
||||
})
|
||||
|
||||
it('sorts cards with subpath nodeIds before higher root IDs', async () => {
|
||||
const { store, groups } = createErrorGroups()
|
||||
store.lastNodeErrors = {
|
||||
'2': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
errors: [{ type: 'err', message: 'Error', details: '' }]
|
||||
},
|
||||
'1:20': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
errors: [{ type: 'err', message: 'Error', details: '' }]
|
||||
},
|
||||
'1': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
errors: [{ type: 'err', message: 'Error', details: '' }]
|
||||
}
|
||||
}
|
||||
await nextTick()
|
||||
|
||||
const execGroup = groups.allErrorGroups.value.find(
|
||||
(g) => g.type === 'execution'
|
||||
)
|
||||
const nodeIds = execGroup?.cards.map((c) => c.nodeId)
|
||||
expect(nodeIds).toEqual(['1', '1:20', '2'])
|
||||
})
|
||||
|
||||
it('sorts deeply nested nodeIds by each segment numerically', async () => {
|
||||
const { store, groups } = createErrorGroups()
|
||||
store.lastNodeErrors = {
|
||||
'10:11:99': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
errors: [{ type: 'err', message: 'Error', details: '' }]
|
||||
},
|
||||
'10:11:12': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
errors: [{ type: 'err', message: 'Error', details: '' }]
|
||||
},
|
||||
'10:2': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
errors: [{ type: 'err', message: 'Error', details: '' }]
|
||||
}
|
||||
}
|
||||
await nextTick()
|
||||
|
||||
const execGroup = groups.allErrorGroups.value.find(
|
||||
(g) => g.type === 'execution'
|
||||
)
|
||||
const nodeIds = execGroup?.cards.map((c) => c.nodeId)
|
||||
expect(nodeIds).toEqual(['10:2', '10:11:12', '10:11:99'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('filteredGroups', () => {
|
||||
|
||||
@@ -23,10 +23,7 @@ import { st } from '@/i18n'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import type { ErrorCardData, ErrorGroup, ErrorItem } from './types'
|
||||
import type { NodeExecutionId } from '@/types/nodeIdentification'
|
||||
import {
|
||||
isNodeExecutionId,
|
||||
compareExecutionId
|
||||
} from '@/types/nodeIdentification'
|
||||
import { isNodeExecutionId } from '@/types/nodeIdentification'
|
||||
|
||||
const PROMPT_CARD_ID = '__prompt__'
|
||||
const SINGLE_GROUP_KEY = '__single__'
|
||||
@@ -154,16 +151,12 @@ function addCardErrorToGroup(
|
||||
group.get(card.id)?.errors.push(error)
|
||||
}
|
||||
|
||||
function compareNodeId(a: ErrorCardData, b: ErrorCardData): number {
|
||||
return compareExecutionId(a.nodeId, b.nodeId)
|
||||
}
|
||||
|
||||
function toSortedGroups(groupsMap: Map<string, GroupEntry>): ErrorGroup[] {
|
||||
return Array.from(groupsMap.entries())
|
||||
.map(([title, groupData]) => ({
|
||||
type: 'execution' as const,
|
||||
title,
|
||||
cards: Array.from(groupData.cards.values()).sort(compareNodeId),
|
||||
cards: Array.from(groupData.cards.values()),
|
||||
priority: groupData.priority
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
|
||||
@@ -17,13 +17,13 @@ export const useNodePaste = <T>(
|
||||
) => {
|
||||
const { onPaste, fileFilter = () => true, allow_batch = false } = options
|
||||
|
||||
node.pasteFiles = function (files: File[]) {
|
||||
node.pasteFiles = async function (files: File[]) {
|
||||
const filteredFiles = Array.from(files).filter(fileFilter)
|
||||
if (!filteredFiles.length) return false
|
||||
|
||||
const paste = allow_batch ? filteredFiles : filteredFiles.slice(0, 1)
|
||||
|
||||
void onPaste(paste)
|
||||
await onPaste(paste)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
export function useQueueFeatureFlags() {
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const isQueuePanelV2Enabled = computed(() =>
|
||||
settingStore.get('Comfy.Queue.QPOV2')
|
||||
)
|
||||
const isRunProgressBarEnabled = computed(
|
||||
() => settingStore.get('Comfy.Queue.ShowRunProgressBar') !== false
|
||||
)
|
||||
|
||||
return {
|
||||
isQueuePanelV2Enabled,
|
||||
isRunProgressBarEnabled
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ function createMockNode(): LGraphNode {
|
||||
return createMockLGraphNode({
|
||||
pos: [0, 0],
|
||||
pasteFile: vi.fn(),
|
||||
pasteFiles: vi.fn()
|
||||
pasteFiles: vi.fn().mockResolvedValue(true)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -201,20 +201,21 @@ describe('pasteImageNodes', () => {
|
||||
const file2 = createImageFile('test2.jpg', 'image/jpeg')
|
||||
|
||||
const result = await pasteImageNodes(mockCanvas, [file1, file2])
|
||||
await result.completion
|
||||
|
||||
expect(createNode).toHaveBeenCalledTimes(2)
|
||||
expect(createNode).toHaveBeenNthCalledWith(1, mockCanvas, 'LoadImage')
|
||||
expect(createNode).toHaveBeenNthCalledWith(2, mockCanvas, 'LoadImage')
|
||||
expect(mockNode1.pasteFile).toHaveBeenCalledWith(file1)
|
||||
expect(mockNode2.pasteFile).toHaveBeenCalledWith(file2)
|
||||
expect(result).toEqual([mockNode1, mockNode2])
|
||||
expect(result.nodes).toEqual([mockNode1, mockNode2])
|
||||
})
|
||||
|
||||
it('should handle empty file list', async () => {
|
||||
const result = await pasteImageNodes(mockCanvas, [])
|
||||
|
||||
expect(createNode).not.toHaveBeenCalled()
|
||||
expect(result).toEqual([])
|
||||
expect(result.nodes).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -57,11 +57,11 @@ function pasteClipboardItems(data: DataTransfer): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
function pasteItemsOnNode(
|
||||
async function pasteItemsOnNode(
|
||||
items: DataTransferItemList,
|
||||
node: LGraphNode | null,
|
||||
contentType: string
|
||||
): void {
|
||||
): Promise<void> {
|
||||
if (!node) return
|
||||
|
||||
const filteredItems = Array.from(items).filter((item) =>
|
||||
@@ -72,10 +72,12 @@ function pasteItemsOnNode(
|
||||
if (!blob) return
|
||||
|
||||
node.pasteFile?.(blob)
|
||||
node.pasteFiles?.(
|
||||
Array.from(filteredItems)
|
||||
.map((i) => i.getAsFile())
|
||||
.filter((f) => f !== null)
|
||||
await Promise.resolve(
|
||||
node.pasteFiles?.(
|
||||
Array.from(filteredItems)
|
||||
.map((i) => i.getAsFile())
|
||||
.filter((f) => f !== null)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -89,27 +91,37 @@ export async function pasteImageNode(
|
||||
imageNode = await createNode(canvas, 'LoadImage')
|
||||
}
|
||||
|
||||
pasteItemsOnNode(items, imageNode, 'image')
|
||||
await pasteItemsOnNode(items, imageNode, 'image')
|
||||
return imageNode
|
||||
}
|
||||
|
||||
interface PasteNodesResult {
|
||||
nodes: LGraphNode[]
|
||||
completion: Promise<void>
|
||||
}
|
||||
|
||||
export async function pasteImageNodes(
|
||||
canvas: LGraphCanvas,
|
||||
fileList: File[]
|
||||
): Promise<LGraphNode[]> {
|
||||
): Promise<PasteNodesResult> {
|
||||
const nodes: LGraphNode[] = []
|
||||
const uploads: Promise<void>[] = []
|
||||
|
||||
for (const file of fileList) {
|
||||
const node = await createNode(canvas, 'LoadImage')
|
||||
if (!node) continue
|
||||
|
||||
nodes.push(node)
|
||||
|
||||
const transfer = new DataTransfer()
|
||||
transfer.items.add(file)
|
||||
const imageNode = await pasteImageNode(canvas, transfer.items)
|
||||
|
||||
if (imageNode) {
|
||||
nodes.push(imageNode)
|
||||
}
|
||||
uploads.push(pasteItemsOnNode(transfer.items, node, 'image'))
|
||||
}
|
||||
|
||||
return nodes
|
||||
return {
|
||||
nodes,
|
||||
completion: Promise.all(uploads).then(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
export async function pasteAudioNode(
|
||||
@@ -120,7 +132,7 @@ export async function pasteAudioNode(
|
||||
if (!audioNode) {
|
||||
audioNode = await createNode(canvas, 'LoadAudio')
|
||||
}
|
||||
pasteItemsOnNode(items, audioNode, 'audio')
|
||||
await pasteItemsOnNode(items, audioNode, 'audio')
|
||||
return audioNode
|
||||
}
|
||||
|
||||
@@ -151,7 +163,7 @@ export async function pasteVideoNode(
|
||||
if (!videoNode) {
|
||||
videoNode = await createNode(canvas, 'LoadVideo')
|
||||
}
|
||||
pasteItemsOnNode(items, videoNode, 'video')
|
||||
await pasteItemsOnNode(items, videoNode, 'video')
|
||||
return videoNode
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import { forEachNode } from '@/utils/graphTraversalUtil'
|
||||
|
||||
import { CanvasPointer } from './CanvasPointer'
|
||||
import type { ContextMenu } from './ContextMenu'
|
||||
import { createCursorCache } from './cursorCache'
|
||||
import { DragAndScale } from './DragAndScale'
|
||||
import type { AnimationOptions } from './DragAndScale'
|
||||
import type { LGraph } from './LGraph'
|
||||
@@ -365,8 +364,6 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
this.canvas.dispatchEvent(new CustomEvent(type, { detail }))
|
||||
}
|
||||
|
||||
private _setCursor!: ReturnType<typeof createCursorCache>
|
||||
|
||||
private _updateCursorStyle() {
|
||||
if (!this.state.shouldSetCursor) return
|
||||
|
||||
@@ -389,7 +386,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
cursor = 'grab'
|
||||
}
|
||||
|
||||
this._setCursor(cursor)
|
||||
this.canvas.style.cursor = cursor
|
||||
}
|
||||
|
||||
// Whether the canvas was previously being dragged prior to pressing space key.
|
||||
@@ -1914,7 +1911,6 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
this.pointer.element = element
|
||||
|
||||
if (!element) return
|
||||
this._setCursor = createCursorCache(element)
|
||||
|
||||
// TODO: classList.add
|
||||
element.className += ' lgraphcanvas'
|
||||
@@ -2974,7 +2970,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
// Set appropriate cursor for resize direction
|
||||
this._setCursor(cursors[resizeDirection])
|
||||
this.canvas.style.cursor = cursors[resizeDirection]
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -3976,14 +3972,41 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
)
|
||||
}
|
||||
|
||||
/** @todo Refactor to where it belongs - e.g. Deleting / creating nodes is not actually canvas event. */
|
||||
/**
|
||||
* Signals the start of a compound graph operation. All graph mutations
|
||||
* between this call and the matching {@link emitAfterChange} are treated
|
||||
* as a single undoable action by the change tracking system.
|
||||
*
|
||||
* Emits a `litegraph:canvas` DOM event with `subType: 'before-change'`,
|
||||
* which `ChangeTracker` listens for to suppress intermediate state
|
||||
* snapshots. Calls are nestable — only the outermost pair triggers a
|
||||
* state check.
|
||||
*
|
||||
* Always pair with {@link emitAfterChange} in a `try/finally` block.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* canvas.emitBeforeChange()
|
||||
* try {
|
||||
* // multiple graph mutations...
|
||||
* } finally {
|
||||
* canvas.emitAfterChange()
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
emitBeforeChange(): void {
|
||||
this.emitEvent({
|
||||
subType: 'before-change'
|
||||
})
|
||||
}
|
||||
|
||||
/** @todo See {@link emitBeforeChange} */
|
||||
/**
|
||||
* Signals the end of a compound graph operation started by
|
||||
* {@link emitBeforeChange}. When the outermost pair completes, the
|
||||
* change tracking system takes a single state snapshot and records
|
||||
* one undo entry for all mutations since the matching
|
||||
* `emitBeforeChange`.
|
||||
*/
|
||||
emitAfterChange(): void {
|
||||
this.emitEvent({
|
||||
subType: 'after-change'
|
||||
@@ -6577,7 +6600,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
const ySizeFix = opts.posSizeFix[1] * LiteGraph.NODE_SLOT_HEIGHT
|
||||
const nodeX = opts.position[0] + opts.posAdd[0] + xSizeFix
|
||||
const nodeY = opts.position[1] + opts.posAdd[1] + ySizeFix
|
||||
const pos: [number, number] = [nodeX, nodeY]
|
||||
const pos = [nodeX, nodeY]
|
||||
const newNode = LiteGraph.createNode(nodeTypeStr, nodeNewOpts?.title, {
|
||||
pos
|
||||
})
|
||||
|
||||
@@ -10,13 +10,7 @@ import { Reroute } from './Reroute'
|
||||
import { InputIndicators } from './canvas/InputIndicators'
|
||||
import { LabelPosition, SlotDirection, SlotShape, SlotType } from './draw'
|
||||
import { Rectangle } from './infrastructure/Rectangle'
|
||||
import type {
|
||||
CreateNodeOptions,
|
||||
Dictionary,
|
||||
ISlotType,
|
||||
Rect,
|
||||
WhenNullish
|
||||
} from './interfaces'
|
||||
import type { Dictionary, ISlotType, Rect, WhenNullish } from './interfaces'
|
||||
import { distance, isInsideRectangle, overlapBounding } from './measure'
|
||||
import { SubgraphIONodeBase } from './subgraph/SubgraphIONodeBase'
|
||||
import { SubgraphSlot } from './subgraph/SubgraphSlotBase'
|
||||
@@ -531,7 +525,7 @@ export class LiteGraphGlobal {
|
||||
createNode(
|
||||
type: string,
|
||||
title?: string,
|
||||
options?: CreateNodeOptions
|
||||
options?: Dictionary<unknown>
|
||||
): LGraphNode | null {
|
||||
const base_class = this.registered_node_types[type]
|
||||
if (!base_class) {
|
||||
@@ -567,7 +561,10 @@ export class LiteGraphGlobal {
|
||||
|
||||
// extra options
|
||||
if (options) {
|
||||
Object.assign(node, options)
|
||||
for (const i in options) {
|
||||
// @ts-expect-error #577 Requires interface
|
||||
node[i] = options[i]
|
||||
}
|
||||
}
|
||||
|
||||
// callback
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { createCursorCache } from './cursorCache'
|
||||
|
||||
function createMockElement() {
|
||||
let cursorValue = ''
|
||||
const setter = vi.fn((value: string) => {
|
||||
cursorValue = value
|
||||
})
|
||||
|
||||
const element = document.createElement('div')
|
||||
Object.defineProperty(element.style, 'cursor', {
|
||||
get: () => cursorValue,
|
||||
set: setter
|
||||
})
|
||||
|
||||
return { element, setter }
|
||||
}
|
||||
|
||||
describe('createCursorCache', () => {
|
||||
it('should only write to DOM when cursor value changes', () => {
|
||||
const { element, setter } = createMockElement()
|
||||
const setCursor = createCursorCache(element)
|
||||
|
||||
setCursor('crosshair')
|
||||
setCursor('crosshair')
|
||||
setCursor('crosshair')
|
||||
|
||||
expect(setter).toHaveBeenCalledTimes(1)
|
||||
expect(setter).toHaveBeenCalledWith('crosshair')
|
||||
})
|
||||
|
||||
it('should write to DOM when cursor value differs', () => {
|
||||
const { element, setter } = createMockElement()
|
||||
const setCursor = createCursorCache(element)
|
||||
|
||||
setCursor('default')
|
||||
setCursor('crosshair')
|
||||
setCursor('grabbing')
|
||||
|
||||
expect(setter).toHaveBeenCalledTimes(3)
|
||||
expect(setter).toHaveBeenNthCalledWith(1, 'default')
|
||||
expect(setter).toHaveBeenNthCalledWith(2, 'crosshair')
|
||||
expect(setter).toHaveBeenNthCalledWith(3, 'grabbing')
|
||||
})
|
||||
|
||||
it('should skip repeated values interspersed with changes', () => {
|
||||
const { element, setter } = createMockElement()
|
||||
const setCursor = createCursorCache(element)
|
||||
|
||||
setCursor('default')
|
||||
setCursor('default')
|
||||
setCursor('grab')
|
||||
setCursor('grab')
|
||||
setCursor('default')
|
||||
|
||||
expect(setter).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
})
|
||||
@@ -1,8 +0,0 @@
|
||||
export function createCursorCache(element: HTMLElement) {
|
||||
let lastCursor = ''
|
||||
return function setCursor(cursor: string) {
|
||||
if (cursor === lastCursor) return
|
||||
lastCursor = cursor
|
||||
element.style.cursor = cursor
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,13 @@ import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
|
||||
import type { TWidgetValue } from '@/lib/litegraph/src/types/widgets'
|
||||
|
||||
import type { ContextMenu } from './ContextMenu'
|
||||
import type { LGraphNode, NodeId, NodeProperty } from './LGraphNode'
|
||||
import type { LGraphNode, NodeId } from './LGraphNode'
|
||||
import type { LLink, LinkId } from './LLink'
|
||||
import type { Reroute, RerouteId } from './Reroute'
|
||||
import type { SubgraphInput } from './subgraph/SubgraphInput'
|
||||
import type { SubgraphInputNode } from './subgraph/SubgraphInputNode'
|
||||
import type { SubgraphOutputNode } from './subgraph/SubgraphOutputNode'
|
||||
import type {
|
||||
LGraphEventMode,
|
||||
LinkDirection,
|
||||
RenderShape
|
||||
} from './types/globalEnums'
|
||||
import type { LinkDirection, RenderShape } from './types/globalEnums'
|
||||
import type { IBaseWidget } from './types/widgets'
|
||||
|
||||
export type Dictionary<T> = { [key: string]: T }
|
||||
@@ -377,22 +373,6 @@ export interface INodeOutputSlot extends INodeSlot {
|
||||
slot_index?: number
|
||||
}
|
||||
|
||||
/** Options for {@link LiteGraphGlobal.createNode}. Shallow-copied onto the new node. */
|
||||
export interface CreateNodeOptions {
|
||||
pos?: Point
|
||||
size?: Size
|
||||
properties?: Dictionary<NodeProperty | undefined>
|
||||
flags?: Partial<INodeFlags>
|
||||
mode?: LGraphEventMode
|
||||
color?: string
|
||||
bgcolor?: string
|
||||
boxcolor?: string
|
||||
title?: string
|
||||
shape?: RenderShape
|
||||
inputs?: Partial<INodeInputSlot>[]
|
||||
outputs?: Partial<INodeOutputSlot>[]
|
||||
}
|
||||
|
||||
/** Links */
|
||||
export interface ConnectingLink extends IInputOrOutput {
|
||||
node: LGraphNode
|
||||
|
||||
@@ -91,7 +91,6 @@ export { RecursionError } from './infrastructure/RecursionError'
|
||||
export type {
|
||||
CanvasColour,
|
||||
ColorOption,
|
||||
CreateNodeOptions,
|
||||
IContextMenuOptions,
|
||||
IContextMenuValue,
|
||||
INodeInputSlot,
|
||||
@@ -145,11 +144,7 @@ export { isColorable } from './utils/type'
|
||||
export { createUuidv4 } from './utils/uuid'
|
||||
export type { UUID } from './utils/uuid'
|
||||
export { truncateText } from './utils/textUtils'
|
||||
export {
|
||||
evaluateInput,
|
||||
getWidgetStep,
|
||||
resolveNodeRootGraphId
|
||||
} from './utils/widget'
|
||||
export { getWidgetStep, resolveNodeRootGraphId } from './utils/widget'
|
||||
export { distributeSpace, type SpaceRequest } from './utils/spaceDistribution'
|
||||
|
||||
export { BaseWidget } from './widgets/BaseWidget'
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
import { evaluateMathExpression } from '@/lib/litegraph/src/utils/mathParser'
|
||||
|
||||
describe('evaluateMathExpression', () => {
|
||||
test.each([
|
||||
['2+3', 5],
|
||||
['10-4', 6],
|
||||
['3*7', 21],
|
||||
['15/3', 5]
|
||||
])('basic arithmetic: %s = %d', (input, expected) => {
|
||||
expect(evaluateMathExpression(input)).toBe(expected)
|
||||
})
|
||||
|
||||
test.each([
|
||||
['2+3*4', 14],
|
||||
['(2+3)*4', 20],
|
||||
['10-2*3', 4],
|
||||
['10/2+3', 8]
|
||||
])('operator precedence: %s = %d', (input, expected) => {
|
||||
expect(evaluateMathExpression(input)).toBe(expected)
|
||||
})
|
||||
|
||||
test.each([
|
||||
['3.14*2', 6.28],
|
||||
['.5+.5', 1],
|
||||
['1.5+2.5', 4],
|
||||
['0.1+0.2', 0.1 + 0.2],
|
||||
['123.', 123],
|
||||
['123.+3', 126]
|
||||
])('decimals: %s', (input, expected) => {
|
||||
expect(evaluateMathExpression(input)).toBe(expected)
|
||||
})
|
||||
|
||||
test.each([
|
||||
[' 2 + 3 ', 5],
|
||||
[' 10 - 4 ', 6],
|
||||
[' ( 2 + 3 ) * 4 ', 20]
|
||||
])('whitespace handling: "%s" = %d', (input, expected) => {
|
||||
expect(evaluateMathExpression(input)).toBe(expected)
|
||||
})
|
||||
|
||||
test.each([
|
||||
['((2+3))', 5],
|
||||
['(1+(2*(3+4)))', 15],
|
||||
['((1+2)*(3+4))', 21]
|
||||
])('nested parentheses: %s = %d', (input, expected) => {
|
||||
expect(evaluateMathExpression(input)).toBe(expected)
|
||||
})
|
||||
|
||||
test.each([
|
||||
['-5', -5],
|
||||
['-(3+2)', -5],
|
||||
['--5', 5],
|
||||
['+5', 5],
|
||||
['-3*2', -6],
|
||||
['2*-3', -6],
|
||||
['1+-2', -1],
|
||||
['2--3', 5],
|
||||
['-2*-3', 6],
|
||||
['-(2+3)*-(4+5)', 45]
|
||||
])('unary operators: %s = %d', (input, expected) => {
|
||||
expect(evaluateMathExpression(input)).toBe(expected)
|
||||
})
|
||||
|
||||
test.each([
|
||||
['2 /2+3 * 4.75- -6', 21.25],
|
||||
['2 / (2 + 3) * 4.33 - -6', 7.732],
|
||||
['12* 123/-(-5 + 2)', 492],
|
||||
['((80 - (19)))', 61],
|
||||
['(1 - 2) + -(-(-(-4)))', 3],
|
||||
['1 - -(-(-(-4)))', -3],
|
||||
['12* 123/(-5 + 2)', -492],
|
||||
['12 * -123', -1476],
|
||||
['((2.33 / (2.9+3.5)*4) - -6)', 7.45625],
|
||||
['123.45*(678.90 / (-2.5+ 11.5)-(80 -19) *33.25) / 20 + 11', -12042.760875],
|
||||
[
|
||||
'(123.45*(678.90 / (-2.5+ 11.5)-(((80 -(19))) *33.25)) / 20) - (123.45*(678.90 / (-2.5+ 11.5)-(((80 -(19))) *33.25)) / 20) + (13 - 2)/ -(-11) ',
|
||||
1
|
||||
]
|
||||
])('complex expression: %s', (input, expected) => {
|
||||
expect(evaluateMathExpression(input)).toBeCloseTo(expected as number)
|
||||
})
|
||||
|
||||
test.each(['', 'abc', '2+', '(2+3', '2+3)', '()', '*3', '2 3', '.', '123..'])(
|
||||
'invalid input returns undefined: "%s"',
|
||||
(input) => {
|
||||
expect(evaluateMathExpression(input)).toBeUndefined()
|
||||
}
|
||||
)
|
||||
|
||||
test('division by zero returns Infinity', () => {
|
||||
expect(evaluateMathExpression('1/0')).toBe(Infinity)
|
||||
})
|
||||
|
||||
test('0/0 returns NaN', () => {
|
||||
expect(evaluateMathExpression('0/0')).toBeNaN()
|
||||
})
|
||||
|
||||
test.each([
|
||||
['10%3', 1],
|
||||
['10%3+1', 2],
|
||||
['7%2', 1]
|
||||
])('modulo: %s = %d', (input, expected) => {
|
||||
expect(evaluateMathExpression(input)).toBe(expected)
|
||||
})
|
||||
|
||||
test('negative zero is normalized to positive zero', () => {
|
||||
expect(Object.is(evaluateMathExpression('-0'), 0)).toBe(true)
|
||||
})
|
||||
|
||||
test('deeply nested parentheses exceeding depth limit returns undefined', () => {
|
||||
const input = '('.repeat(201) + '1' + ')'.repeat(201)
|
||||
expect(evaluateMathExpression(input)).toBeUndefined()
|
||||
})
|
||||
|
||||
test('parentheses within depth limit evaluate correctly', () => {
|
||||
const input = '('.repeat(200) + '1' + ')'.repeat(200)
|
||||
expect(evaluateMathExpression(input)).toBe(1)
|
||||
})
|
||||
})
|
||||
@@ -1,116 +0,0 @@
|
||||
type Token = { type: 'number'; value: number } | { type: 'op'; value: string }
|
||||
|
||||
function tokenize(input: string): Token[] | undefined {
|
||||
const tokens: Token[] = []
|
||||
const re = /(\d+(?:\.\d*)?|\.\d+)|([+\-*/%()])/g
|
||||
let lastIndex = 0
|
||||
|
||||
for (const match of input.matchAll(re)) {
|
||||
const gap = input.slice(lastIndex, match.index)
|
||||
if (gap.trim()) return undefined
|
||||
lastIndex = match.index + match[0].length
|
||||
|
||||
if (match[1]) tokens.push({ type: 'number', value: parseFloat(match[1]) })
|
||||
else tokens.push({ type: 'op', value: match[2] })
|
||||
}
|
||||
|
||||
if (input.slice(lastIndex).trim()) return undefined
|
||||
return tokens
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a basic arithmetic expression string containing
|
||||
* `+`, `-`, `*`, `/`, `%`, parentheses, and decimal numbers.
|
||||
* Returns `undefined` for empty or malformed input.
|
||||
*/
|
||||
export function evaluateMathExpression(input: string): number | undefined {
|
||||
const tokenized = tokenize(input)
|
||||
if (!tokenized || tokenized.length === 0) return undefined
|
||||
|
||||
const tokens: Token[] = tokenized
|
||||
let pos = 0
|
||||
let depth = 0
|
||||
const MAX_DEPTH = 200
|
||||
|
||||
function peek(): Token | undefined {
|
||||
return tokens[pos]
|
||||
}
|
||||
|
||||
function consume(): Token {
|
||||
return tokens[pos++]
|
||||
}
|
||||
|
||||
function primary(): number | undefined {
|
||||
const t = peek()
|
||||
if (!t) return undefined
|
||||
|
||||
if (t.type === 'number') {
|
||||
consume()
|
||||
return t.value
|
||||
}
|
||||
|
||||
if (t.type === 'op' && t.value === '(') {
|
||||
if (++depth > MAX_DEPTH) return undefined
|
||||
consume()
|
||||
const result = expr()
|
||||
if (result === undefined) return undefined
|
||||
const closing = peek()
|
||||
if (!closing || closing.type !== 'op' || closing.value !== ')') {
|
||||
return undefined
|
||||
}
|
||||
consume()
|
||||
depth--
|
||||
return result
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
function unary(): number | undefined {
|
||||
const t = peek()
|
||||
if (t?.type === 'op' && (t.value === '+' || t.value === '-')) {
|
||||
consume()
|
||||
const operand = unary()
|
||||
if (operand === undefined) return undefined
|
||||
return t.value === '-' ? -operand : operand
|
||||
}
|
||||
return primary()
|
||||
}
|
||||
|
||||
function factor(): number | undefined {
|
||||
let left = unary()
|
||||
if (left === undefined) return undefined
|
||||
|
||||
while (
|
||||
peek()?.type === 'op' &&
|
||||
(peek()!.value === '*' || peek()!.value === '/' || peek()!.value === '%')
|
||||
) {
|
||||
const op = consume().value
|
||||
const right = unary()
|
||||
if (right === undefined) return undefined
|
||||
left =
|
||||
op === '*' ? left * right : op === '/' ? left / right : left % right
|
||||
}
|
||||
return left
|
||||
}
|
||||
|
||||
function expr(): number | undefined {
|
||||
let left = factor()
|
||||
if (left === undefined) return undefined
|
||||
|
||||
while (
|
||||
peek()?.type === 'op' &&
|
||||
(peek()!.value === '+' || peek()!.value === '-')
|
||||
) {
|
||||
const op = consume().value
|
||||
const right = factor()
|
||||
if (right === undefined) return undefined
|
||||
left = op === '+' ? left + right : left - right
|
||||
}
|
||||
return left
|
||||
}
|
||||
|
||||
const result = expr()
|
||||
if (result === undefined || pos !== tokens.length) return undefined
|
||||
return result === 0 ? 0 : result
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { describe, expect, test } from 'vitest'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { IWidgetOptions } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
evaluateInput,
|
||||
getWidgetStep,
|
||||
resolveNodeRootGraphId
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
@@ -71,57 +70,3 @@ describe('resolveNodeRootGraphId', () => {
|
||||
expect(resolveNodeRootGraphId(node, 'app-root-id')).toBe('app-root-id')
|
||||
})
|
||||
})
|
||||
|
||||
describe('evaluateInput', () => {
|
||||
test.each([
|
||||
['42', 42],
|
||||
['3.14', 3.14],
|
||||
['-7', -7],
|
||||
['0', 0]
|
||||
])('plain number: "%s" = %d', (input, expected) => {
|
||||
expect(evaluateInput(input)).toBe(expected)
|
||||
})
|
||||
|
||||
test.each([
|
||||
['2+3', 5],
|
||||
['(4+2)*3', 18],
|
||||
['3.14*2', 6.28],
|
||||
['10/2+3', 8]
|
||||
])('expression: "%s" = %d', (input, expected) => {
|
||||
expect(evaluateInput(input)).toBe(expected)
|
||||
})
|
||||
|
||||
test('empty string returns 0 (Number("") === 0)', () => {
|
||||
expect(evaluateInput('')).toBe(0)
|
||||
})
|
||||
|
||||
test.each(['abc', 'hello world'])(
|
||||
'invalid input returns undefined: "%s"',
|
||||
(input) => {
|
||||
expect(evaluateInput(input)).toBeUndefined()
|
||||
}
|
||||
)
|
||||
|
||||
test('division by zero returns undefined', () => {
|
||||
expect(evaluateInput('1/0')).toBeUndefined()
|
||||
})
|
||||
|
||||
test('0/0 returns undefined (NaN is filtered)', () => {
|
||||
expect(evaluateInput('0/0')).toBeUndefined()
|
||||
})
|
||||
|
||||
test('scientific notation via Number() fallback', () => {
|
||||
expect(evaluateInput('1e5')).toBe(100000)
|
||||
})
|
||||
|
||||
test('hex notation via Number() fallback', () => {
|
||||
expect(evaluateInput('0xff')).toBe(255)
|
||||
})
|
||||
|
||||
test.each(['Infinity', '-Infinity'])(
|
||||
'"%s" returns undefined (non-finite rejected)',
|
||||
(input) => {
|
||||
expect(evaluateInput(input)).toBeUndefined()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -2,8 +2,6 @@ import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { UUID } from '@/lib/litegraph/src/utils/uuid'
|
||||
|
||||
import { evaluateMathExpression } from '@/lib/litegraph/src/utils/mathParser'
|
||||
|
||||
/**
|
||||
* The step value for numeric widgets.
|
||||
* Use {@link IWidgetOptions.step2} if available, otherwise fallback to
|
||||
@@ -14,13 +12,17 @@ export function getWidgetStep(options: IWidgetOptions<unknown>): number {
|
||||
}
|
||||
|
||||
export function evaluateInput(input: string): number | undefined {
|
||||
const result = evaluateMathExpression(input)
|
||||
if (result !== undefined) {
|
||||
if (!isFinite(result)) return undefined
|
||||
return result
|
||||
// Check if v is a valid equation or a number
|
||||
if (/^[\d\s.()*+/-]+$/.test(input)) {
|
||||
// Solve the equation if possible
|
||||
try {
|
||||
input = eval(input)
|
||||
} catch {
|
||||
// Ignore eval errors
|
||||
}
|
||||
}
|
||||
const newValue = Number(input)
|
||||
if (!isFinite(newValue)) return undefined
|
||||
if (isNaN(newValue)) return undefined
|
||||
return newValue
|
||||
}
|
||||
|
||||
|
||||
@@ -344,10 +344,6 @@
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "تبديل وضع التركيز"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_apps": {
|
||||
"label": "تبديل الشريط الجانبي للتطبيقات",
|
||||
"tooltip": "التطبيقات"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "تبديل الشريط الجانبي للأصول",
|
||||
"tooltip": "الأصول"
|
||||
|
||||
@@ -326,40 +326,35 @@
|
||||
"deleteWorkflow": "حذف سير العمل",
|
||||
"duplicate": "تكرار",
|
||||
"enterAppMode": "الدخول إلى وضع التطبيق",
|
||||
"enterBuilderMode": "دخول وضع بناء التطبيق",
|
||||
"enterNewName": "أدخل اسمًا جديدًا",
|
||||
"exitAppMode": "الخروج من وضع التطبيق",
|
||||
"missingNodesWarning": "يحتوي سير العمل على عقد غير مدعومة (مظللة باللون الأحمر).",
|
||||
"workflowActions": "إجراءات سير العمل"
|
||||
},
|
||||
"builderMenu": {
|
||||
"exitAppBuilder": "الخروج من مُنشئ التطبيق"
|
||||
"exitAppBuilder": "الخروج من مُنشئ التطبيق",
|
||||
"saveApp": "حفظ التطبيق"
|
||||
},
|
||||
"builderToolbar": {
|
||||
"app": "تطبيق",
|
||||
"appDescription": "يفتح كتطبيق بشكل افتراضي",
|
||||
"arrange": "معاينة",
|
||||
"arrangeDescription": "مراجعة تخطيط التطبيق",
|
||||
"backToWorkflow": "العودة إلى سير العمل",
|
||||
"connectOutput": "توصيل مخرج",
|
||||
"connectOutputBody1": "يجب توصيل مخرج واحد على الأقل قبل حفظ التطبيق.",
|
||||
"connectOutputBody2": "انتقل إلى خطوة 'تحديد' وانقر على عقد المخرجات لإضافتها هنا.",
|
||||
"defaultModeAppliedAppBody": "سيفتح سير العمل هذا في وضع التطبيق بشكل افتراضي من الآن فصاعدًا.",
|
||||
"defaultModeAppliedAppPrompt": "هل ترغب في عرضه الآن؟",
|
||||
"defaultModeAppliedGraphBody": "سيفتح سير العمل هذا كـمخطط عقد بشكل افتراضي من الآن فصاعدًا.",
|
||||
"defaultModeAppliedGraphPrompt": "هل ترغب في عرض التطبيق مع ذلك؟",
|
||||
"defaultModeAppliedTitle": "تم التعيين بنجاح",
|
||||
"defaultView": "تعيين العرض الافتراضي",
|
||||
"defaultViewDescription": "اختر كيفية الفتح",
|
||||
"defaultViewLabel": "افتراضيًا، سيتم فتح سير العمل هذا كـ:",
|
||||
"defaultViewTitle": "تعيين العرض الافتراضي لهذا سير العمل",
|
||||
"emptyWorkflowExplanation": "سير العمل الخاص بك فارغ. تحتاج إلى بعض العقد أولاً للبدء في بناء تطبيق.",
|
||||
"emptyWorkflowPrompt": "هل ترغب في البدء بقالب؟",
|
||||
"emptyWorkflowTitle": "لا يحتوي سير العمل هذا على أي عقد",
|
||||
"filename": "اسم الملف",
|
||||
"label": "منشئ التطبيقات",
|
||||
"loadTemplate": "تحميل قالب",
|
||||
"nodeGraph": "رسم العقد",
|
||||
"nodeGraphDescription": "يفتح كرسم عقد بشكل افتراضي",
|
||||
"save": "حفظ",
|
||||
"saveAs": "حفظ باسم",
|
||||
"saveAsLabel": "احفظ سير العمل هذا كـ ...",
|
||||
"saveDescription": "حفظ وإنهاء",
|
||||
"saveSuccess": "تم الحفظ بنجاح",
|
||||
"saveSuccessAppMessage": "تم حفظ '{name}'. سيفتح في وضع التطبيق بشكل افتراضي من الآن فصاعدًا.",
|
||||
"saveSuccessAppPrompt": "هل ترغب في عرضه الآن؟",
|
||||
"saveSuccessGraphMessage": "تم حفظ '{name}'. سيفتح كرسم عقد بشكل افتراضي.",
|
||||
"select": "تحديد",
|
||||
"selectDescription": "اختيار المدخلات/المخرجات",
|
||||
"switchToSelect": "الانتقال إلى التحديد",
|
||||
@@ -1339,9 +1334,7 @@
|
||||
"linearMode": {
|
||||
"appModeToolbar": {
|
||||
"appBuilder": "منشئ التطبيقات",
|
||||
"apps": "التطبيقات",
|
||||
"appsEmptyMessage": "سيتم عرض التطبيقات المحفوظة هنا.\nانقر أدناه لبناء تطبيقك الأول.",
|
||||
"enterAppMode": "الدخول إلى وضع التطبيق"
|
||||
"apps": "التطبيقات"
|
||||
},
|
||||
"arrange": {
|
||||
"atLeastOne": "عقدة واحدة على الأقل",
|
||||
@@ -1366,16 +1359,13 @@
|
||||
"outputsExample": "أمثلة: \"حفظ صورة\" أو \"حفظ فيديو\"",
|
||||
"promptAddInputs": "انقر على معلمات العقدة لإضافتها هنا كمدخلات",
|
||||
"promptAddOutputs": "انقر على عقد الإخراج لإضافتها هنا. هذه ستكون النتائج المُولدة.",
|
||||
"title": "وضع بناء التطبيق",
|
||||
"unknownWidget": "عنصر الواجهة غير مرئي"
|
||||
"title": "وضع بناء التطبيق"
|
||||
},
|
||||
"downloadAll": "تنزيل الكل",
|
||||
"dragAndDropImage": "اسحب وأسقط صورة",
|
||||
"enterNodeGraph": "دخول مخطط العقد",
|
||||
"giveFeedback": "إعطاء ملاحظات",
|
||||
"graphMode": "وضع الرسم البياني",
|
||||
"linearMode": "وضع التطبيق",
|
||||
"mobileControls": "تعديل وتشغيل",
|
||||
"queue": {
|
||||
"clear": "مسح قائمة الانتظار",
|
||||
"clickToClear": "انقر لمسح قائمة الانتظار"
|
||||
@@ -1383,7 +1373,6 @@
|
||||
"rerun": "تشغيل مجدد",
|
||||
"reuseParameters": "إعادة استخدام المعلمات",
|
||||
"runCount": "عدد مرات التشغيل:",
|
||||
"viewJob": "عرض المهمة",
|
||||
"welcome": {
|
||||
"backToWorkflow": "العودة إلى سير العمل",
|
||||
"buildApp": "إنشاء تطبيق",
|
||||
@@ -1760,7 +1749,6 @@
|
||||
"disabled": "معطل",
|
||||
"disabledTooltip": "لن يتم وضع سير العمل في قائمة الانتظار تلقائيًا",
|
||||
"execute": "تنفيذ",
|
||||
"fullscreen": "ملء الشاشة",
|
||||
"help": "مساعدة",
|
||||
"helpAndFeedback": "المساعدة والتعليقات",
|
||||
"hideMenu": "إخفاء القائمة",
|
||||
@@ -1908,8 +1896,7 @@
|
||||
"Workflows": "سير العمل",
|
||||
"Zoom In": "تكبير",
|
||||
"Zoom Out": "تصغير",
|
||||
"Zoom to fit": "تكبير لتناسب",
|
||||
"linearMode_appModeToolbar_apps": "linearMode.appModeToolbar.apps"
|
||||
"Zoom to fit": "تكبير لتناسب"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "ألوان العقد",
|
||||
@@ -3215,9 +3202,7 @@
|
||||
"enterFilename": "أدخل اسم الملف",
|
||||
"enterFilenamePrompt": "أدخل اسم الملف:",
|
||||
"exportWorkflow": "تصدير سير العمل",
|
||||
"saveWorkflow": "حفظ سير العمل",
|
||||
"savedAsApp": "تم التحويل إلى سير عمل تطبيق",
|
||||
"savedAsWorkflow": "تم التحويل إلى سير عمل مخطط العقد فقط"
|
||||
"saveWorkflow": "حفظ سير العمل"
|
||||
},
|
||||
"workspace": {
|
||||
"addedToWorkspace": "تمت إضافتك إلى {workspaceName}",
|
||||
|
||||
@@ -344,10 +344,6 @@
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "Toggle Focus Mode"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_apps": {
|
||||
"label": "Toggle Apps Sidebar",
|
||||
"tooltip": "Apps"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "Toggle Assets Sidebar",
|
||||
"tooltip": "Assets"
|
||||
|
||||
@@ -854,7 +854,6 @@
|
||||
"clearQueued": "Clear queued",
|
||||
"clearHistory": "Clear job history",
|
||||
"dockedJobHistory": "Docked Job History",
|
||||
"showRunProgressBar": "Show run progress bar",
|
||||
"clearHistoryMenuAssetsNote": "Media assets won't be deleted.",
|
||||
"filterJobs": "Filter jobs",
|
||||
"filterBy": "Filter by",
|
||||
@@ -1281,6 +1280,7 @@
|
||||
"Duplicate Current Workflow": "Duplicate Current Workflow",
|
||||
"Export": "Export",
|
||||
"Export (API)": "Export (API)",
|
||||
"Share": "Share",
|
||||
"Convert Selection to Subgraph": "Convert Selection to Subgraph",
|
||||
"Edit Subgraph Widgets": "Edit Subgraph Widgets",
|
||||
"Exit Subgraph": "Exit Subgraph",
|
||||
@@ -1351,7 +1351,6 @@
|
||||
"Toggle Essential Bottom Panel": "Toggle Essential Bottom Panel",
|
||||
"Toggle View Controls Bottom Panel": "Toggle View Controls Bottom Panel",
|
||||
"Focus Mode": "Focus Mode",
|
||||
"linearMode_appModeToolbar_apps": "linearMode.appModeToolbar.apps",
|
||||
"Assets": "Assets",
|
||||
"Model Library": "Model Library",
|
||||
"Node Library": "Node Library",
|
||||
|
||||
@@ -344,10 +344,6 @@
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "Alternar Modo de Enfoque"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_apps": {
|
||||
"label": "Alternar barra lateral de aplicaciones",
|
||||
"tooltip": "Aplicaciones"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "Alternar barra lateral de recursos",
|
||||
"tooltip": "Recursos"
|
||||
|
||||
@@ -326,40 +326,35 @@
|
||||
"deleteWorkflow": "Eliminar flujo de trabajo",
|
||||
"duplicate": "Duplicar",
|
||||
"enterAppMode": "Entrar en modo aplicación",
|
||||
"enterBuilderMode": "Entrar al constructor de aplicaciones",
|
||||
"enterNewName": "Ingrese un nuevo nombre",
|
||||
"exitAppMode": "Salir del modo aplicación",
|
||||
"missingNodesWarning": "El flujo de trabajo contiene nodos no compatibles (resaltados en rojo).",
|
||||
"workflowActions": "Acciones del flujo de trabajo"
|
||||
},
|
||||
"builderMenu": {
|
||||
"exitAppBuilder": "Salir del constructor de aplicaciones"
|
||||
"exitAppBuilder": "Salir del constructor de aplicaciones",
|
||||
"saveApp": "Guardar aplicación"
|
||||
},
|
||||
"builderToolbar": {
|
||||
"app": "Aplicación",
|
||||
"appDescription": "Se abre como una aplicación por defecto",
|
||||
"arrange": "Vista previa",
|
||||
"arrangeDescription": "Revisar el diseño de la aplicación",
|
||||
"backToWorkflow": "Volver al flujo de trabajo",
|
||||
"connectOutput": "Conectar una salida",
|
||||
"connectOutputBody1": "Tu aplicación necesita al menos una salida conectada antes de poder guardarse.",
|
||||
"connectOutputBody2": "Cambia al paso 'Seleccionar' y haz clic en los nodos de salida para agregarlos aquí.",
|
||||
"defaultModeAppliedAppBody": "Este flujo de trabajo se abrirá en Modo de Aplicación por defecto a partir de ahora.",
|
||||
"defaultModeAppliedAppPrompt": "¿Te gustaría verlo ahora?",
|
||||
"defaultModeAppliedGraphBody": "Este flujo de trabajo se abrirá como un grafo de nodos por defecto a partir de ahora.",
|
||||
"defaultModeAppliedGraphPrompt": "¿Aún quieres ver la aplicación?",
|
||||
"defaultModeAppliedTitle": "Configurado correctamente",
|
||||
"defaultView": "Establecer vista predeterminada",
|
||||
"defaultViewDescription": "Elige cómo se abre esto",
|
||||
"defaultViewLabel": "Por defecto, este flujo de trabajo se abrirá como:",
|
||||
"defaultViewTitle": "Establecer la vista predeterminada para este flujo de trabajo",
|
||||
"emptyWorkflowExplanation": "Tu flujo de trabajo está vacío. Necesitas algunos nodos para empezar a crear una aplicación.",
|
||||
"emptyWorkflowPrompt": "¿Quieres empezar con una plantilla?",
|
||||
"emptyWorkflowTitle": "Este flujo de trabajo no tiene nodos",
|
||||
"filename": "Nombre de archivo",
|
||||
"label": "Constructor de aplicaciones",
|
||||
"loadTemplate": "Cargar una plantilla",
|
||||
"nodeGraph": "Grafo de nodos",
|
||||
"nodeGraphDescription": "Se abre como grafo de nodos por defecto",
|
||||
"save": "Guardar",
|
||||
"saveAs": "Guardar como",
|
||||
"saveAsLabel": "Guardar este flujo de trabajo como...",
|
||||
"saveDescription": "Guardar y finalizar",
|
||||
"saveSuccess": "Guardado exitosamente",
|
||||
"saveSuccessAppMessage": "'{name}' ha sido guardado. Se abrirá en modo aplicación por defecto de ahora en adelante.",
|
||||
"saveSuccessAppPrompt": "¿Te gustaría verlo ahora?",
|
||||
"saveSuccessGraphMessage": "'{name}' ha sido guardado. Se abrirá como grafo de nodos por defecto.",
|
||||
"select": "Seleccionar",
|
||||
"selectDescription": "Elegir entradas/salidas",
|
||||
"switchToSelect": "Cambiar a Seleccionar",
|
||||
@@ -1339,9 +1334,7 @@
|
||||
"linearMode": {
|
||||
"appModeToolbar": {
|
||||
"appBuilder": "Constructor de aplicaciones",
|
||||
"apps": "Aplicaciones",
|
||||
"appsEmptyMessage": "Las aplicaciones guardadas aparecerán aquí.\nHaz clic abajo para crear tu primera aplicación.",
|
||||
"enterAppMode": "Entrar en modo de aplicación"
|
||||
"apps": "Aplicaciones"
|
||||
},
|
||||
"arrange": {
|
||||
"atLeastOne": "al menos uno",
|
||||
@@ -1366,16 +1359,13 @@
|
||||
"outputsExample": "Ejemplos: “Guardar imagen” o “Guardar video”",
|
||||
"promptAddInputs": "Haz clic en los parámetros del nodo para agregarlos aquí como entradas",
|
||||
"promptAddOutputs": "Haz clic en los nodos de salida para agregarlos aquí. Estos serán los resultados generados.",
|
||||
"title": "Modo constructor de aplicaciones",
|
||||
"unknownWidget": "Widget no visible"
|
||||
"title": "Modo constructor de aplicaciones"
|
||||
},
|
||||
"downloadAll": "Descargar todo",
|
||||
"dragAndDropImage": "Arrastra y suelta una imagen",
|
||||
"enterNodeGraph": "Entrar al grafo de nodos",
|
||||
"giveFeedback": "Enviar comentarios",
|
||||
"graphMode": "Modo gráfico",
|
||||
"linearMode": "Modo App",
|
||||
"mobileControls": "Editar y ejecutar",
|
||||
"queue": {
|
||||
"clear": "Limpiar cola",
|
||||
"clickToClear": "Haz clic para limpiar la cola"
|
||||
@@ -1383,7 +1373,6 @@
|
||||
"rerun": "Volver a ejecutar",
|
||||
"reuseParameters": "Reutilizar parámetros",
|
||||
"runCount": "Número de ejecuciones:",
|
||||
"viewJob": "Ver tarea",
|
||||
"welcome": {
|
||||
"backToWorkflow": "Volver al flujo de trabajo",
|
||||
"buildApp": "Crear aplicación",
|
||||
@@ -1760,7 +1749,6 @@
|
||||
"disabled": "Deshabilitado",
|
||||
"disabledTooltip": "El flujo de trabajo no se encolará automáticamente",
|
||||
"execute": "Ejecutar",
|
||||
"fullscreen": "Pantalla completa",
|
||||
"help": "Ayuda",
|
||||
"helpAndFeedback": "Ayuda y comentarios",
|
||||
"hideMenu": "Ocultar menú",
|
||||
@@ -1908,8 +1896,7 @@
|
||||
"Workflows": "Flujos de trabajo",
|
||||
"Zoom In": "Acercar",
|
||||
"Zoom Out": "Alejar",
|
||||
"Zoom to fit": "Ajustar al tamaño",
|
||||
"linearMode_appModeToolbar_apps": "linearMode.appModeToolbar.apps"
|
||||
"Zoom to fit": "Ajustar al tamaño"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Colores de nodos",
|
||||
@@ -3215,9 +3202,7 @@
|
||||
"enterFilename": "Introduzca el nombre del archivo",
|
||||
"enterFilenamePrompt": "Introduce el nombre del archivo:",
|
||||
"exportWorkflow": "Exportar flujo de trabajo",
|
||||
"saveWorkflow": "Guardar flujo de trabajo",
|
||||
"savedAsApp": "Convertido a flujo de trabajo de aplicación",
|
||||
"savedAsWorkflow": "Convertido a flujo de trabajo solo de grafo de nodos"
|
||||
"saveWorkflow": "Guardar flujo de trabajo"
|
||||
},
|
||||
"workspace": {
|
||||
"addedToWorkspace": "Has sido añadido a {workspaceName}",
|
||||
|
||||
@@ -344,10 +344,6 @@
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "تغییر حالت تمرکز"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_apps": {
|
||||
"label": "نمایش/پنهان کردن نوار کناری اپلیکیشنها",
|
||||
"tooltip": "اپلیکیشنها"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "تغییر نوار کناری داراییها",
|
||||
"tooltip": "داراییها"
|
||||
|
||||
@@ -326,40 +326,35 @@
|
||||
"deleteWorkflow": "حذف workflow",
|
||||
"duplicate": "تکرار",
|
||||
"enterAppMode": "ورود به حالت اپلیکیشن",
|
||||
"enterBuilderMode": "ورود به حالت سازنده اپلیکیشن",
|
||||
"enterNewName": "نام جدید را وارد کنید",
|
||||
"exitAppMode": "خروج از حالت اپلیکیشن",
|
||||
"missingNodesWarning": "workflow شامل نودهای پشتیبانینشده است (با رنگ قرمز مشخص شدهاند).",
|
||||
"workflowActions": "عملیات گردشکار"
|
||||
},
|
||||
"builderMenu": {
|
||||
"exitAppBuilder": "خروج از سازنده برنامه"
|
||||
"exitAppBuilder": "خروج از سازنده برنامه",
|
||||
"saveApp": "ذخیره برنامه"
|
||||
},
|
||||
"builderToolbar": {
|
||||
"app": "اپلیکیشن",
|
||||
"appDescription": "به طور پیشفرض به صورت اپلیکیشن باز میشود",
|
||||
"arrange": "پیشنمایش",
|
||||
"arrangeDescription": "بررسی چیدمان اپلیکیشن",
|
||||
"backToWorkflow": "بازگشت به ورکفلو",
|
||||
"connectOutput": "اتصال خروجی",
|
||||
"connectOutputBody1": "اپلیکیشن شما باید حداقل یک خروجی متصل داشته باشد تا بتوان آن را ذخیره کرد.",
|
||||
"connectOutputBody2": "به مرحله «انتخاب» بروید و روی nodeهای خروجی کلیک کنید تا اینجا اضافه شوند.",
|
||||
"defaultModeAppliedAppBody": "این ورکفلو از این پس به طور پیشفرض در حالت اپلیکیشن باز خواهد شد.",
|
||||
"defaultModeAppliedAppPrompt": "آیا مایل هستید اکنون آن را مشاهده کنید؟",
|
||||
"defaultModeAppliedGraphBody": "این ورکفلو از این پس به طور پیشفرض به صورت گراف node باز خواهد شد.",
|
||||
"defaultModeAppliedGraphPrompt": "آیا همچنان مایل به مشاهده اپلیکیشن هستید؟",
|
||||
"defaultModeAppliedTitle": "با موفقیت تنظیم شد",
|
||||
"defaultView": "تنظیم نمای پیشفرض",
|
||||
"defaultViewDescription": "انتخاب نحوه باز شدن",
|
||||
"defaultViewLabel": "به طور پیشفرض، این گردشکار به صورت زیر باز میشود:",
|
||||
"defaultViewTitle": "تنظیم نمای پیشفرض برای این گردشکار",
|
||||
"emptyWorkflowExplanation": "ورکفلو شما خالی است. ابتدا باید چند node اضافه کنید تا بتوانید اپلیکیشن بسازید.",
|
||||
"emptyWorkflowPrompt": "آیا میخواهید با یک قالب شروع کنید؟",
|
||||
"emptyWorkflowTitle": "این ورکفلو هیچ node ندارد",
|
||||
"filename": "نام فایل",
|
||||
"label": "سازنده اپلیکیشن",
|
||||
"loadTemplate": "بارگذاری قالب",
|
||||
"nodeGraph": "گراف node",
|
||||
"nodeGraphDescription": "به طور پیشفرض به صورت گراف node باز میشود",
|
||||
"save": "ذخیره",
|
||||
"saveAs": "ذخیره به عنوان",
|
||||
"saveAsLabel": "این گردشکار را ذخیره کنید به عنوان ...",
|
||||
"saveDescription": "ذخیره و پایان",
|
||||
"saveSuccess": "با موفقیت ذخیره شد",
|
||||
"saveSuccessAppMessage": "'{name}' ذخیره شد. از این پس به طور پیشفرض در حالت اپلیکیشن باز خواهد شد.",
|
||||
"saveSuccessAppPrompt": "آیا مایلید اکنون آن را مشاهده کنید؟",
|
||||
"saveSuccessGraphMessage": "'{name}' ذخیره شد. به طور پیشفرض به صورت گراف node باز خواهد شد.",
|
||||
"select": "انتخاب",
|
||||
"selectDescription": "انتخاب ورودی/خروجیها",
|
||||
"switchToSelect": "رفتن به انتخاب",
|
||||
@@ -1339,9 +1334,7 @@
|
||||
"linearMode": {
|
||||
"appModeToolbar": {
|
||||
"appBuilder": "سازنده اپلیکیشن",
|
||||
"apps": "اپلیکیشنها",
|
||||
"appsEmptyMessage": "اپلیکیشنهای ذخیرهشده اینجا نمایش داده میشوند.\nبرای ساخت اولین اپلیکیشن خود، روی دکمه زیر کلیک کنید.",
|
||||
"enterAppMode": "ورود به حالت اپلیکیشن"
|
||||
"apps": "اپلیکیشنها"
|
||||
},
|
||||
"arrange": {
|
||||
"atLeastOne": "یک",
|
||||
@@ -1366,16 +1359,13 @@
|
||||
"outputsExample": "مثالها: «ذخیره تصویر» یا «ذخیره ویدیو»",
|
||||
"promptAddInputs": "برای افزودن پارامترها به عنوان ورودی، روی پارامترهای گره کلیک کنید",
|
||||
"promptAddOutputs": "برای افزودن خروجی، روی گرههای خروجی کلیک کنید. اینها نتایج تولیدشده خواهند بود.",
|
||||
"title": "حالت ساخت اپلیکیشن",
|
||||
"unknownWidget": "ویجت قابل مشاهده نیست"
|
||||
"title": "حالت ساخت اپلیکیشن"
|
||||
},
|
||||
"downloadAll": "دانلود همه",
|
||||
"dragAndDropImage": "تصویر را بکشید و رها کنید",
|
||||
"enterNodeGraph": "ورود به گراف node",
|
||||
"giveFeedback": "ارسال بازخورد",
|
||||
"graphMode": "حالت گراف",
|
||||
"linearMode": "حالت برنامه",
|
||||
"mobileControls": "ویرایش و اجرا",
|
||||
"queue": {
|
||||
"clear": "پاکسازی صف",
|
||||
"clickToClear": "برای پاکسازی صف کلیک کنید"
|
||||
@@ -1383,7 +1373,6 @@
|
||||
"rerun": "اجرای مجدد",
|
||||
"reuseParameters": "استفاده مجدد از پارامترها",
|
||||
"runCount": "تعداد اجرا: ",
|
||||
"viewJob": "مشاهده وظیفه",
|
||||
"welcome": {
|
||||
"backToWorkflow": "بازگشت به گردشکار",
|
||||
"buildApp": "ساخت اپلیکیشن",
|
||||
@@ -1760,7 +1749,6 @@
|
||||
"disabled": "غیرفعال",
|
||||
"disabledTooltip": "workflow به صورت خودکار در صف قرار نمیگیرد",
|
||||
"execute": "اجرا",
|
||||
"fullscreen": "تمامصفحه",
|
||||
"help": "راهنما",
|
||||
"helpAndFeedback": "راهنما و بازخورد",
|
||||
"hideMenu": "مخفی کردن منو",
|
||||
@@ -1908,8 +1896,7 @@
|
||||
"Workflows": "Workflowها",
|
||||
"Zoom In": "بزرگنمایی",
|
||||
"Zoom Out": "کوچکنمایی",
|
||||
"Zoom to fit": "بزرگنمایی برای تطبیق",
|
||||
"linearMode_appModeToolbar_apps": "linearMode.appModeToolbar.apps"
|
||||
"Zoom to fit": "بزرگنمایی برای تطبیق"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "رنگهای نود",
|
||||
@@ -3227,9 +3214,7 @@
|
||||
"enterFilename": "نام فایل را وارد کنید",
|
||||
"enterFilenamePrompt": "نام فایل را وارد کنید:",
|
||||
"exportWorkflow": "خروجی گرفتن از workflow",
|
||||
"saveWorkflow": "ذخیره workflow",
|
||||
"savedAsApp": "به گردشکار اپلیکیشن تبدیل شد",
|
||||
"savedAsWorkflow": "به گردشکار فقط گراف node تبدیل شد"
|
||||
"saveWorkflow": "ذخیره workflow"
|
||||
},
|
||||
"workspace": {
|
||||
"addedToWorkspace": "شما به {workspaceName} اضافه شدید",
|
||||
|
||||
@@ -344,10 +344,6 @@
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "Basculer le mode focus"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_apps": {
|
||||
"label": "Basculer la barre latérale des applications",
|
||||
"tooltip": "Applications"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "Afficher/Masquer la barre latérale des ressources",
|
||||
"tooltip": "Ressources"
|
||||
|
||||
@@ -326,44 +326,39 @@
|
||||
"deleteWorkflow": "Supprimer le workflow",
|
||||
"duplicate": "Dupliquer",
|
||||
"enterAppMode": "Entrer en mode application",
|
||||
"enterBuilderMode": "Entrer dans le mode constructeur d'application",
|
||||
"enterNewName": "Entrez un nouveau nom",
|
||||
"exitAppMode": "Quitter le mode application",
|
||||
"missingNodesWarning": "Le workflow contient des nœuds non pris en charge (surlignés en rouge).",
|
||||
"workflowActions": "Actions du workflow"
|
||||
},
|
||||
"builderMenu": {
|
||||
"exitAppBuilder": "Quitter le constructeur d'application"
|
||||
"exitAppBuilder": "Quitter le constructeur d'application",
|
||||
"saveApp": "Enregistrer l'application"
|
||||
},
|
||||
"builderToolbar": {
|
||||
"app": "Application",
|
||||
"appDescription": "S'ouvre par défaut en tant qu'application",
|
||||
"arrange": "Aperçu",
|
||||
"arrangeDescription": "Vérifier la disposition de l'application",
|
||||
"backToWorkflow": "Retour au workflow",
|
||||
"connectOutput": "Connecter une sortie",
|
||||
"connectOutputBody1": "Votre application doit avoir au moins une sortie connectée avant de pouvoir être enregistrée.",
|
||||
"connectOutputBody2": "Passez à l'étape « Sélectionner » et cliquez sur les nœuds de sortie pour les ajouter ici.",
|
||||
"defaultModeAppliedAppBody": "Ce workflow s’ouvrira désormais par défaut en mode Application.",
|
||||
"defaultModeAppliedAppPrompt": "Voulez-vous le voir maintenant ?",
|
||||
"defaultModeAppliedGraphBody": "Ce workflow s’ouvrira désormais par défaut sous forme de graphe de nœuds.",
|
||||
"defaultModeAppliedGraphPrompt": "Voulez-vous tout de même voir l’application ?",
|
||||
"defaultModeAppliedTitle": "Défini avec succès",
|
||||
"defaultView": "Définir une vue par défaut",
|
||||
"defaultViewDescription": "Choisissez comment cela s'ouvre",
|
||||
"defaultViewLabel": "Par défaut, ce workflow s'ouvrira comme :",
|
||||
"defaultViewTitle": "Définir la vue par défaut pour ce workflow",
|
||||
"emptyWorkflowExplanation": "Votre workflow est vide. Vous devez d’abord ajouter des nœuds pour commencer à créer une application.",
|
||||
"emptyWorkflowPrompt": "Voulez-vous commencer avec un modèle ?",
|
||||
"emptyWorkflowTitle": "Ce workflow ne contient aucun nœud",
|
||||
"filename": "Nom du fichier",
|
||||
"label": "Créateur d'applications",
|
||||
"loadTemplate": "Charger un modèle",
|
||||
"nodeGraph": "Graphe de nœuds",
|
||||
"nodeGraphDescription": "S'ouvre par défaut en tant que graphe de nœuds",
|
||||
"save": "Enregistrer",
|
||||
"saveAs": "Enregistrer sous",
|
||||
"saveAsLabel": "Enregistrer ce workflow sous ...",
|
||||
"saveDescription": "Enregistrer et terminer",
|
||||
"saveSuccess": "Enregistré avec succès",
|
||||
"saveSuccessAppMessage": "« {name} » a été enregistré. Il s'ouvrira désormais par défaut en mode application.",
|
||||
"saveSuccessAppPrompt": "Voulez-vous le voir maintenant ?",
|
||||
"saveSuccessGraphMessage": "« {name} » a été enregistré. Il s'ouvrira par défaut en tant que graphe de nœuds.",
|
||||
"select": "Sélectionner",
|
||||
"selectDescription": "Choisir les entrées/sorties",
|
||||
"switchToSelect": "Passer à Sélectionner",
|
||||
"viewApp": "Voir l’application"
|
||||
"viewApp": "Voir l'application"
|
||||
},
|
||||
"clipboard": {
|
||||
"errorMessage": "Échec de la copie dans le presse-papiers",
|
||||
@@ -1339,9 +1334,7 @@
|
||||
"linearMode": {
|
||||
"appModeToolbar": {
|
||||
"appBuilder": "Créateur d'applications",
|
||||
"apps": "Applications",
|
||||
"appsEmptyMessage": "Les applications enregistrées apparaîtront ici.\nCliquez ci-dessous pour créer votre première application.",
|
||||
"enterAppMode": "Entrer en mode application"
|
||||
"apps": "Applications"
|
||||
},
|
||||
"arrange": {
|
||||
"atLeastOne": "au moins un",
|
||||
@@ -1366,16 +1359,13 @@
|
||||
"outputsExample": "Exemples : « Enregistrer l’image » ou « Enregistrer la vidéo »",
|
||||
"promptAddInputs": "Cliquez sur les paramètres du nœud pour les ajouter ici comme entrées",
|
||||
"promptAddOutputs": "Cliquez sur les nœuds de sortie pour les ajouter ici. Ce seront les résultats générés.",
|
||||
"title": "Mode créateur d’application",
|
||||
"unknownWidget": "Widget non visible"
|
||||
"title": "Mode créateur d’application"
|
||||
},
|
||||
"downloadAll": "Tout télécharger",
|
||||
"dragAndDropImage": "Glissez-déposez une image",
|
||||
"enterNodeGraph": "Entrer dans le graphique de nœuds",
|
||||
"giveFeedback": "Donner un avis",
|
||||
"graphMode": "Mode graphique",
|
||||
"linearMode": "Mode App",
|
||||
"mobileControls": "Éditer & Exécuter",
|
||||
"queue": {
|
||||
"clear": "Vider la file d'attente",
|
||||
"clickToClear": "Cliquez pour vider la file d'attente"
|
||||
@@ -1383,7 +1373,6 @@
|
||||
"rerun": "Relancer",
|
||||
"reuseParameters": "Réutiliser les paramètres",
|
||||
"runCount": "Nombre d’exécutions :",
|
||||
"viewJob": "Voir la tâche",
|
||||
"welcome": {
|
||||
"backToWorkflow": "Retour au workflow",
|
||||
"buildApp": "Créer une application",
|
||||
@@ -1760,7 +1749,6 @@
|
||||
"disabled": "Désactivé",
|
||||
"disabledTooltip": "Le flux de travail ne sera pas mis en file d'attente automatiquement",
|
||||
"execute": "Exécuter",
|
||||
"fullscreen": "Plein écran",
|
||||
"help": "Aide",
|
||||
"helpAndFeedback": "Aide et commentaires",
|
||||
"hideMenu": "Masquer le menu",
|
||||
@@ -1908,8 +1896,7 @@
|
||||
"Workflows": "Flux de travail",
|
||||
"Zoom In": "Zoom avant",
|
||||
"Zoom Out": "Zoom arrière",
|
||||
"Zoom to fit": "Ajuster à l'écran",
|
||||
"linearMode_appModeToolbar_apps": "linearMode.appModeToolbar.apps"
|
||||
"Zoom to fit": "Ajuster à l'écran"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Couleurs des nœuds",
|
||||
@@ -3215,9 +3202,7 @@
|
||||
"enterFilename": "Entrez le nom du fichier",
|
||||
"enterFilenamePrompt": "Entrez le nom du fichier :",
|
||||
"exportWorkflow": "Exporter le flux de travail",
|
||||
"saveWorkflow": "Enregistrer le flux de travail",
|
||||
"savedAsApp": "Converti en workflow d'application",
|
||||
"savedAsWorkflow": "Converti en workflow graphique de nœuds uniquement"
|
||||
"saveWorkflow": "Enregistrer le flux de travail"
|
||||
},
|
||||
"workspace": {
|
||||
"addedToWorkspace": "Vous avez été ajouté à {workspaceName}",
|
||||
|
||||
@@ -344,10 +344,6 @@
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "フォーカスモードの切り替え"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_apps": {
|
||||
"label": "アプリサイドバーを切り替え",
|
||||
"tooltip": "アプリ"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "アセットサイドバーの表示切り替え",
|
||||
"tooltip": "アセット"
|
||||
|
||||
@@ -326,40 +326,35 @@
|
||||
"deleteWorkflow": "ワークフローを削除",
|
||||
"duplicate": "複製",
|
||||
"enterAppMode": "アプリモードに入る",
|
||||
"enterBuilderMode": "アプリビルダーに入る",
|
||||
"enterNewName": "新しい名前を入力",
|
||||
"exitAppMode": "アプリモードを終了",
|
||||
"missingNodesWarning": "ワークフローに未対応のノードが含まれています(赤でハイライト)。",
|
||||
"workflowActions": "ワークフロー操作"
|
||||
},
|
||||
"builderMenu": {
|
||||
"exitAppBuilder": "アプリビルダーを終了"
|
||||
"exitAppBuilder": "アプリビルダーを終了",
|
||||
"saveApp": "アプリを保存"
|
||||
},
|
||||
"builderToolbar": {
|
||||
"app": "アプリ",
|
||||
"appDescription": "デフォルトでアプリとして開きます",
|
||||
"arrange": "プレビュー",
|
||||
"arrangeDescription": "アプリのレイアウトを確認",
|
||||
"backToWorkflow": "ワークフローに戻る",
|
||||
"connectOutput": "出力を接続",
|
||||
"connectOutputBody1": "アプリを保存するには、少なくとも1つの出力を接続する必要があります。",
|
||||
"connectOutputBody2": "「選択」ステップに切り替えて、出力ノードをクリックしてここに追加してください。",
|
||||
"defaultModeAppliedAppBody": "このワークフローは今後デフォルトでアプリモードで開きます。",
|
||||
"defaultModeAppliedAppPrompt": "今すぐ表示しますか?",
|
||||
"defaultModeAppliedGraphBody": "このワークフローは今後デフォルトでノードグラフとして開きます。",
|
||||
"defaultModeAppliedGraphPrompt": "アプリを引き続き表示しますか?",
|
||||
"defaultModeAppliedTitle": "正常に設定されました",
|
||||
"defaultView": "デフォルトビューを設定",
|
||||
"defaultViewDescription": "開き方を選択してください",
|
||||
"defaultViewLabel": "デフォルトでは、このワークフローは次のように開きます:",
|
||||
"defaultViewTitle": "このワークフローのデフォルトビューを設定",
|
||||
"emptyWorkflowExplanation": "ワークフローが空です。アプリを作成するにはまずノードが必要です。",
|
||||
"emptyWorkflowPrompt": "テンプレートから始めますか?",
|
||||
"emptyWorkflowTitle": "このワークフローにはノードがありません",
|
||||
"filename": "ファイル名",
|
||||
"label": "アプリビルダー",
|
||||
"loadTemplate": "テンプレートを読み込む",
|
||||
"nodeGraph": "ノードグラフ",
|
||||
"nodeGraphDescription": "デフォルトでノードグラフとして開きます",
|
||||
"save": "保存",
|
||||
"saveAs": "名前を付けて保存",
|
||||
"saveAsLabel": "このワークフローを次の形式で保存...",
|
||||
"saveDescription": "保存して終了",
|
||||
"saveSuccess": "保存に成功しました",
|
||||
"saveSuccessAppMessage": "「{name}」が保存されました。今後はデフォルトでアプリモードで開きます。",
|
||||
"saveSuccessAppPrompt": "今すぐ表示しますか?",
|
||||
"saveSuccessGraphMessage": "「{name}」が保存されました。今後はデフォルトでノードグラフとして開きます。",
|
||||
"select": "選択",
|
||||
"selectDescription": "入力/出力を選択",
|
||||
"switchToSelect": "選択に切り替え",
|
||||
@@ -1339,9 +1334,7 @@
|
||||
"linearMode": {
|
||||
"appModeToolbar": {
|
||||
"appBuilder": "アプリビルダー",
|
||||
"apps": "アプリ",
|
||||
"appsEmptyMessage": "保存されたアプリはここに表示されます。\n下をクリックして最初のアプリを作成しましょう。",
|
||||
"enterAppMode": "アプリモードに入る"
|
||||
"apps": "アプリ"
|
||||
},
|
||||
"arrange": {
|
||||
"atLeastOne": "少なくとも1つ",
|
||||
@@ -1366,16 +1359,13 @@
|
||||
"outputsExample": "例:「画像を保存」「動画を保存」",
|
||||
"promptAddInputs": "ノードのパラメータをクリックして、ここに入力として追加してください",
|
||||
"promptAddOutputs": "出力ノードをクリックしてここに追加してください。これが生成される結果となります。",
|
||||
"title": "アプリビルダーモード",
|
||||
"unknownWidget": "ウィジェットが表示されていません"
|
||||
"title": "アプリビルダーモード"
|
||||
},
|
||||
"downloadAll": "すべてダウンロード",
|
||||
"dragAndDropImage": "画像をドラッグ&ドロップ",
|
||||
"enterNodeGraph": "ノードグラフに入る",
|
||||
"giveFeedback": "フィードバックを送る",
|
||||
"graphMode": "グラフモード",
|
||||
"linearMode": "アプリモード",
|
||||
"mobileControls": "編集と実行",
|
||||
"queue": {
|
||||
"clear": "キューをクリア",
|
||||
"clickToClear": "クリックしてキューをクリア"
|
||||
@@ -1383,7 +1373,6 @@
|
||||
"rerun": "再実行",
|
||||
"reuseParameters": "パラメータを再利用",
|
||||
"runCount": "実行回数:",
|
||||
"viewJob": "ジョブを表示",
|
||||
"welcome": {
|
||||
"backToWorkflow": "ワークフローに戻る",
|
||||
"buildApp": "アプリを作成",
|
||||
@@ -1760,7 +1749,6 @@
|
||||
"disabled": "無効",
|
||||
"disabledTooltip": "ワークフローは自動的にキューに追加されません",
|
||||
"execute": "実行",
|
||||
"fullscreen": "全画面表示",
|
||||
"help": "ヘルプ",
|
||||
"helpAndFeedback": "ヘルプとフィードバック",
|
||||
"hideMenu": "メニューを隠す",
|
||||
@@ -1908,8 +1896,7 @@
|
||||
"Workflows": "ワークフロー",
|
||||
"Zoom In": "ズームイン",
|
||||
"Zoom Out": "ズームアウト",
|
||||
"Zoom to fit": "全体表示にズーム",
|
||||
"linearMode_appModeToolbar_apps": "linearMode.appModeToolbar.apps"
|
||||
"Zoom to fit": "全体表示にズーム"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "ノードの色",
|
||||
@@ -3215,9 +3202,7 @@
|
||||
"enterFilename": "ファイル名を入力",
|
||||
"enterFilenamePrompt": "ファイル名を入力してください:",
|
||||
"exportWorkflow": "ワークフローをエクスポート",
|
||||
"saveWorkflow": "ワークフローを保存",
|
||||
"savedAsApp": "アプリワークフローに変換されました",
|
||||
"savedAsWorkflow": "ノードグラフのみのワークフローに変換されました"
|
||||
"saveWorkflow": "ワークフローを保存"
|
||||
},
|
||||
"workspace": {
|
||||
"addedToWorkspace": "{workspaceName}に追加されました",
|
||||
|
||||
@@ -344,10 +344,6 @@
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "포커스 모드 토글"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_apps": {
|
||||
"label": "앱 사이드바 전환",
|
||||
"tooltip": "앱"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "에셋 사이드바 전환",
|
||||
"tooltip": "에셋"
|
||||
|
||||
@@ -326,40 +326,35 @@
|
||||
"deleteWorkflow": "워크플로 삭제",
|
||||
"duplicate": "복제",
|
||||
"enterAppMode": "앱 모드로 진입",
|
||||
"enterBuilderMode": "앱 빌더로 진입",
|
||||
"enterNewName": "새 이름 입력",
|
||||
"exitAppMode": "앱 모드 종료",
|
||||
"missingNodesWarning": "워크플로우에 지원되지 않는 노드가 포함되어 있습니다(빨간색으로 표시됨).",
|
||||
"workflowActions": "워크플로우 작업"
|
||||
},
|
||||
"builderMenu": {
|
||||
"exitAppBuilder": "앱 빌더 종료"
|
||||
"exitAppBuilder": "앱 빌더 종료",
|
||||
"saveApp": "앱 저장"
|
||||
},
|
||||
"builderToolbar": {
|
||||
"app": "앱",
|
||||
"appDescription": "기본적으로 앱으로 열립니다",
|
||||
"arrange": "미리보기",
|
||||
"arrangeDescription": "앱 레이아웃 검토",
|
||||
"backToWorkflow": "워크플로우로 돌아가기",
|
||||
"connectOutput": "출력 연결",
|
||||
"connectOutputBody1": "앱을 저장하려면 최소 한 개의 출력이 연결되어야 합니다.",
|
||||
"connectOutputBody2": "'선택' 단계로 전환한 후 출력 노드를 클릭하여 여기에 추가하세요.",
|
||||
"defaultModeAppliedAppBody": "이 워크플로우는 앞으로 기본적으로 앱 모드에서 열립니다.",
|
||||
"defaultModeAppliedAppPrompt": "지금 확인하시겠습니까?",
|
||||
"defaultModeAppliedGraphBody": "이 워크플로우는 앞으로 기본적으로 노드 그래프로 열립니다.",
|
||||
"defaultModeAppliedGraphPrompt": "앱을 계속 보시겠습니까?",
|
||||
"defaultModeAppliedTitle": "성공적으로 설정됨",
|
||||
"defaultView": "기본 보기 설정",
|
||||
"defaultViewDescription": "이 워크플로우가 어떻게 열릴지 선택하세요",
|
||||
"defaultViewLabel": "기본적으로 이 워크플로우는 다음과 같이 열립니다:",
|
||||
"defaultViewTitle": "이 워크플로우의 기본 보기 설정",
|
||||
"emptyWorkflowExplanation": "워크플로우가 비어 있습니다. 앱을 만들려면 먼저 노드가 필요합니다.",
|
||||
"emptyWorkflowPrompt": "템플릿으로 시작하시겠습니까?",
|
||||
"emptyWorkflowTitle": "이 워크플로우에는 노드가 없습니다",
|
||||
"filename": "파일명",
|
||||
"label": "앱 빌더",
|
||||
"loadTemplate": "템플릿 불러오기",
|
||||
"nodeGraph": "노드 그래프",
|
||||
"nodeGraphDescription": "기본적으로 노드 그래프로 열립니다",
|
||||
"save": "저장",
|
||||
"saveAs": "다른 이름으로 저장",
|
||||
"saveAsLabel": "이 워크플로우를 다음으로 저장 ...",
|
||||
"saveDescription": "저장 및 완료",
|
||||
"saveSuccess": "성공적으로 저장되었습니다",
|
||||
"saveSuccessAppMessage": "'{name}'이(가) 저장되었습니다. 앞으로 기본적으로 앱 모드로 열립니다.",
|
||||
"saveSuccessAppPrompt": "지금 확인하시겠습니까?",
|
||||
"saveSuccessGraphMessage": "'{name}'이(가) 저장되었습니다. 앞으로 기본적으로 노드 그래프로 열립니다.",
|
||||
"select": "선택",
|
||||
"selectDescription": "입력/출력 선택",
|
||||
"switchToSelect": "선택으로 전환",
|
||||
@@ -1339,9 +1334,7 @@
|
||||
"linearMode": {
|
||||
"appModeToolbar": {
|
||||
"appBuilder": "앱 빌더",
|
||||
"apps": "앱",
|
||||
"appsEmptyMessage": "저장된 앱이 여기에 표시됩니다.\n아래를 클릭하여 첫 번째 앱을 만들어보세요.",
|
||||
"enterAppMode": "앱 모드로 들어가기"
|
||||
"apps": "앱"
|
||||
},
|
||||
"arrange": {
|
||||
"atLeastOne": "최소 한 개",
|
||||
@@ -1366,16 +1359,13 @@
|
||||
"outputsExample": "예시: “이미지 저장” 또는 “비디오 저장”",
|
||||
"promptAddInputs": "노드 파라미터를 클릭하여 입력값으로 추가하세요",
|
||||
"promptAddOutputs": "출력 노드를 클릭하여 여기에 추가하세요. 이들이 생성된 결과가 됩니다.",
|
||||
"title": "앱 빌더 모드",
|
||||
"unknownWidget": "위젯이 표시되지 않습니다"
|
||||
"title": "앱 빌더 모드"
|
||||
},
|
||||
"downloadAll": "모두 다운로드",
|
||||
"dragAndDropImage": "이미지를 드래그 앤 드롭하세요",
|
||||
"enterNodeGraph": "노드 그래프로 진입",
|
||||
"giveFeedback": "피드백 보내기",
|
||||
"graphMode": "그래프 모드",
|
||||
"linearMode": "앱 모드",
|
||||
"mobileControls": "편집 및 실행",
|
||||
"queue": {
|
||||
"clear": "대기열 비우기",
|
||||
"clickToClear": "클릭하여 대기열 비우기"
|
||||
@@ -1383,7 +1373,6 @@
|
||||
"rerun": "다시 실행",
|
||||
"reuseParameters": "파라미터 재사용",
|
||||
"runCount": "실행 횟수:",
|
||||
"viewJob": "작업 보기",
|
||||
"welcome": {
|
||||
"backToWorkflow": "워크플로우로 돌아가기",
|
||||
"buildApp": "앱 만들기",
|
||||
@@ -1760,7 +1749,6 @@
|
||||
"disabled": "비활성화됨",
|
||||
"disabledTooltip": "워크플로 작업을 자동으로 실행 대기열에 추가하지 않습니다.",
|
||||
"execute": "실행",
|
||||
"fullscreen": "전체 화면",
|
||||
"help": "도움말",
|
||||
"helpAndFeedback": "도움말 및 피드백",
|
||||
"hideMenu": "메뉴 숨기기",
|
||||
@@ -1908,8 +1896,7 @@
|
||||
"Workflows": "워크플로",
|
||||
"Zoom In": "확대",
|
||||
"Zoom Out": "축소",
|
||||
"Zoom to fit": "화면에 맞추기",
|
||||
"linearMode_appModeToolbar_apps": "linearMode.appModeToolbar.apps"
|
||||
"Zoom to fit": "화면에 맞추기"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "노드 색상",
|
||||
@@ -3215,9 +3202,7 @@
|
||||
"enterFilename": "파일 이름 입력",
|
||||
"enterFilenamePrompt": "파일 이름을 입력하세요:",
|
||||
"exportWorkflow": "워크플로 내보내기",
|
||||
"saveWorkflow": "워크플로 저장",
|
||||
"savedAsApp": "앱 워크플로우로 변환됨",
|
||||
"savedAsWorkflow": "노드 그래프 전용 워크플로우로 변환됨"
|
||||
"saveWorkflow": "워크플로 저장"
|
||||
},
|
||||
"workspace": {
|
||||
"addedToWorkspace": "{workspaceName} 워크스페이스에 추가되었습니다",
|
||||
|
||||
@@ -344,10 +344,6 @@
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "Alternar modo de foco"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_apps": {
|
||||
"label": "Alternar Barra Lateral de Apps",
|
||||
"tooltip": "Apps"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "Alternar barra lateral de assets",
|
||||
"tooltip": "Assets"
|
||||
|
||||
@@ -326,40 +326,35 @@
|
||||
"deleteWorkflow": "Excluir Fluxo de Trabalho",
|
||||
"duplicate": "Duplicar",
|
||||
"enterAppMode": "Entrar no modo de aplicativo",
|
||||
"enterBuilderMode": "Entrar no construtor de aplicativos",
|
||||
"enterNewName": "Digite um novo nome",
|
||||
"exitAppMode": "Sair do modo de aplicativo",
|
||||
"missingNodesWarning": "O fluxo de trabalho contém nós não suportados (destacados em vermelho).",
|
||||
"workflowActions": "Ações do fluxo de trabalho"
|
||||
},
|
||||
"builderMenu": {
|
||||
"exitAppBuilder": "Sair do construtor de aplicativos"
|
||||
"exitAppBuilder": "Sair do construtor de aplicativos",
|
||||
"saveApp": "Salvar aplicativo"
|
||||
},
|
||||
"builderToolbar": {
|
||||
"app": "Aplicativo",
|
||||
"appDescription": "Abre como aplicativo por padrão",
|
||||
"arrange": "Pré-visualizar",
|
||||
"arrangeDescription": "Revisar layout do aplicativo",
|
||||
"backToWorkflow": "Voltar ao fluxo de trabalho",
|
||||
"connectOutput": "Conectar uma saída",
|
||||
"connectOutputBody1": "Seu aplicativo precisa de pelo menos uma saída conectada antes de ser salvo.",
|
||||
"connectOutputBody2": "Altere para a etapa 'Selecionar' e clique nos nós de saída para adicioná-los aqui.",
|
||||
"defaultModeAppliedAppBody": "Este fluxo de trabalho será aberto no Modo de Aplicativo por padrão a partir de agora.",
|
||||
"defaultModeAppliedAppPrompt": "Gostaria de visualizá-lo agora?",
|
||||
"defaultModeAppliedGraphBody": "Este fluxo de trabalho será aberto como um grafo de nós por padrão a partir de agora.",
|
||||
"defaultModeAppliedGraphPrompt": "Ainda deseja visualizar o aplicativo?",
|
||||
"defaultModeAppliedTitle": "Definido com sucesso",
|
||||
"defaultView": "Definir visualização padrão",
|
||||
"defaultViewDescription": "Escolha como isso será aberto",
|
||||
"defaultViewLabel": "Por padrão, este fluxo será aberto como:",
|
||||
"defaultViewTitle": "Definir a visualização padrão para este fluxo",
|
||||
"emptyWorkflowExplanation": "Seu fluxo de trabalho está vazio. Você precisa de alguns nós para começar a criar um aplicativo.",
|
||||
"emptyWorkflowPrompt": "Deseja começar com um modelo?",
|
||||
"emptyWorkflowTitle": "Este fluxo de trabalho não possui nós",
|
||||
"filename": "Nome do arquivo",
|
||||
"label": "Construtor de aplicativos",
|
||||
"loadTemplate": "Carregar um modelo",
|
||||
"nodeGraph": "Grafo de nós",
|
||||
"nodeGraphDescription": "Abre como grafo de nós por padrão",
|
||||
"save": "Salvar",
|
||||
"saveAs": "Salvar como",
|
||||
"saveAsLabel": "Salvar este fluxo de trabalho como ...",
|
||||
"saveDescription": "Salvar e finalizar",
|
||||
"saveSuccess": "Salvo com sucesso",
|
||||
"saveSuccessAppMessage": "'{name}' foi salvo. Ele abrirá no Modo de Aplicativo por padrão a partir de agora.",
|
||||
"saveSuccessAppPrompt": "Gostaria de visualizá-lo agora?",
|
||||
"saveSuccessGraphMessage": "'{name}' foi salvo. Ele abrirá como grafo de nós por padrão.",
|
||||
"select": "Selecionar",
|
||||
"selectDescription": "Escolha entradas/saídas",
|
||||
"switchToSelect": "Ir para Selecionar",
|
||||
@@ -1339,9 +1334,7 @@
|
||||
"linearMode": {
|
||||
"appModeToolbar": {
|
||||
"appBuilder": "Construtor de aplicativos",
|
||||
"apps": "Aplicativos",
|
||||
"appsEmptyMessage": "Os aplicativos salvos aparecerão aqui.\nClique abaixo para criar seu primeiro aplicativo.",
|
||||
"enterAppMode": "Entrar no modo de aplicativo"
|
||||
"apps": "Aplicativos"
|
||||
},
|
||||
"arrange": {
|
||||
"atLeastOne": "pelo menos um",
|
||||
@@ -1366,16 +1359,13 @@
|
||||
"outputsExample": "Exemplos: “Salvar imagem” ou “Salvar vídeo”",
|
||||
"promptAddInputs": "Clique nos parâmetros do nó para adicioná-los aqui como entradas",
|
||||
"promptAddOutputs": "Clique nos nós de saída para adicioná-los aqui. Estes serão os resultados gerados.",
|
||||
"title": "Modo construtor de app",
|
||||
"unknownWidget": "Widget não visível"
|
||||
"title": "Modo construtor de app"
|
||||
},
|
||||
"downloadAll": "Baixar tudo",
|
||||
"dragAndDropImage": "Arraste e solte uma imagem",
|
||||
"enterNodeGraph": "Entrar no grafo de nós",
|
||||
"giveFeedback": "Enviar feedback",
|
||||
"graphMode": "Modo Gráfico",
|
||||
"linearMode": "Modo App",
|
||||
"mobileControls": "Editar e Executar",
|
||||
"queue": {
|
||||
"clear": "Limpar fila",
|
||||
"clickToClear": "Clique para limpar a fila"
|
||||
@@ -1383,7 +1373,6 @@
|
||||
"rerun": "Executar novamente",
|
||||
"reuseParameters": "Reutilizar parâmetros",
|
||||
"runCount": "Número de execuções:",
|
||||
"viewJob": "Ver tarefa",
|
||||
"welcome": {
|
||||
"backToWorkflow": "Voltar ao fluxo de trabalho",
|
||||
"buildApp": "Criar aplicativo",
|
||||
@@ -1760,7 +1749,6 @@
|
||||
"disabled": "Desativado",
|
||||
"disabledTooltip": "O fluxo de trabalho não será enfileirado automaticamente",
|
||||
"execute": "Executar",
|
||||
"fullscreen": "Tela cheia",
|
||||
"help": "Ajuda",
|
||||
"helpAndFeedback": "Ajuda e feedback",
|
||||
"hideMenu": "Ocultar menu",
|
||||
@@ -1908,8 +1896,7 @@
|
||||
"Workflows": "Fluxos de trabalho",
|
||||
"Zoom In": "Aumentar zoom",
|
||||
"Zoom Out": "Diminuir zoom",
|
||||
"Zoom to fit": "Ajustar zoom",
|
||||
"linearMode_appModeToolbar_apps": "linearMode.appModeToolbar.apps"
|
||||
"Zoom to fit": "Ajustar zoom"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Cores dos Nós",
|
||||
@@ -3227,9 +3214,7 @@
|
||||
"enterFilename": "Digite o nome do arquivo",
|
||||
"enterFilenamePrompt": "Digite o nome do arquivo:",
|
||||
"exportWorkflow": "Exportar Fluxo de Trabalho",
|
||||
"saveWorkflow": "Salvar fluxo de trabalho",
|
||||
"savedAsApp": "Convertido para fluxo de aplicativo",
|
||||
"savedAsWorkflow": "Convertido para fluxo apenas com grafo de nós"
|
||||
"saveWorkflow": "Salvar fluxo de trabalho"
|
||||
},
|
||||
"workspace": {
|
||||
"addedToWorkspace": "Você foi adicionado ao {workspaceName}",
|
||||
|
||||
@@ -344,10 +344,6 @@
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "Переключить режим фокуса"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_apps": {
|
||||
"label": "Переключить боковую панель приложений",
|
||||
"tooltip": "Приложения"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "Переключить боковую панель ресурсов",
|
||||
"tooltip": "Ресурсы"
|
||||
|
||||
@@ -326,40 +326,35 @@
|
||||
"deleteWorkflow": "Удалить рабочий процесс",
|
||||
"duplicate": "Дублировать",
|
||||
"enterAppMode": "Войти в режим приложения",
|
||||
"enterBuilderMode": "Войти в режим конструктора приложения",
|
||||
"enterNewName": "Введите новое имя",
|
||||
"exitAppMode": "Выйти из режима приложения",
|
||||
"missingNodesWarning": "В рабочем процессе есть неподдерживаемые узлы (выделены красным).",
|
||||
"workflowActions": "Действия с рабочим процессом"
|
||||
},
|
||||
"builderMenu": {
|
||||
"exitAppBuilder": "Выйти из конструктора приложений"
|
||||
"exitAppBuilder": "Выйти из конструктора приложений",
|
||||
"saveApp": "Сохранить приложение"
|
||||
},
|
||||
"builderToolbar": {
|
||||
"app": "Приложение",
|
||||
"appDescription": "Открывается как приложение по умолчанию",
|
||||
"arrange": "Предпросмотр",
|
||||
"arrangeDescription": "Проверьте макет приложения",
|
||||
"backToWorkflow": "Назад к рабочему процессу",
|
||||
"connectOutput": "Подключить выход",
|
||||
"connectOutputBody1": "Вашему приложению необходимо подключить хотя бы один выход перед сохранением.",
|
||||
"connectOutputBody2": "Переключитесь на шаг «Выбрать» и кликните по выходным узлам, чтобы добавить их сюда.",
|
||||
"defaultModeAppliedAppBody": "Этот рабочий процесс теперь будет открываться в режиме приложения по умолчанию.",
|
||||
"defaultModeAppliedAppPrompt": "Хотите просмотреть его сейчас?",
|
||||
"defaultModeAppliedGraphBody": "Этот рабочий процесс теперь будет открываться как граф узлов по умолчанию.",
|
||||
"defaultModeAppliedGraphPrompt": "Хотите всё равно просмотреть приложение?",
|
||||
"defaultModeAppliedTitle": "Успешно установлено",
|
||||
"defaultView": "Установить вид по умолчанию",
|
||||
"defaultViewDescription": "Выберите, как это будет открываться",
|
||||
"defaultViewLabel": "По умолчанию этот рабочий процесс будет открываться как:",
|
||||
"defaultViewTitle": "Установить вид по умолчанию для этого рабочего процесса",
|
||||
"emptyWorkflowExplanation": "Ваш рабочий процесс пуст. Для начала создания приложения добавьте несколько узлов.",
|
||||
"emptyWorkflowPrompt": "Хотите начать с шаблона?",
|
||||
"emptyWorkflowTitle": "В этом рабочем процессе нет узлов",
|
||||
"filename": "Имя файла",
|
||||
"label": "Конструктор приложений",
|
||||
"loadTemplate": "Загрузить шаблон",
|
||||
"nodeGraph": "Граф узлов",
|
||||
"nodeGraphDescription": "Открывается как граф узлов по умолчанию",
|
||||
"save": "Сохранить",
|
||||
"saveAs": "Сохранить как",
|
||||
"saveAsLabel": "Сохранить этот рабочий процесс как ...",
|
||||
"saveDescription": "Сохранить и завершить",
|
||||
"saveSuccess": "Успешно сохранено",
|
||||
"saveSuccessAppMessage": "«{name}» сохранено. Теперь будет открываться в режиме приложения по умолчанию.",
|
||||
"saveSuccessAppPrompt": "Хотите просмотреть сейчас?",
|
||||
"saveSuccessGraphMessage": "«{name}» сохранено. Теперь будет открываться как граф узлов по умолчанию.",
|
||||
"select": "Выбрать",
|
||||
"selectDescription": "Выберите входы/выходы",
|
||||
"switchToSelect": "Переключиться на выбор",
|
||||
@@ -1339,9 +1334,7 @@
|
||||
"linearMode": {
|
||||
"appModeToolbar": {
|
||||
"appBuilder": "Конструктор приложений",
|
||||
"apps": "Приложения",
|
||||
"appsEmptyMessage": "Сохранённые приложения появятся здесь.\nНажмите ниже, чтобы создать своё первое приложение.",
|
||||
"enterAppMode": "Войти в режим приложения"
|
||||
"apps": "Приложения"
|
||||
},
|
||||
"arrange": {
|
||||
"atLeastOne": "хотя бы один",
|
||||
@@ -1366,16 +1359,13 @@
|
||||
"outputsExample": "Примеры: «Сохранить изображение» или «Сохранить видео»",
|
||||
"promptAddInputs": "Нажмите на параметры узла, чтобы добавить их сюда как входные данные",
|
||||
"promptAddOutputs": "Нажмите на выходные узлы, чтобы добавить их сюда. Это будут сгенерированные результаты.",
|
||||
"title": "Режим конструктора приложений",
|
||||
"unknownWidget": "Виджет не отображается"
|
||||
"title": "Режим конструктора приложений"
|
||||
},
|
||||
"downloadAll": "Скачать всё",
|
||||
"dragAndDropImage": "Перетащите изображение",
|
||||
"enterNodeGraph": "Войти в граф узлов",
|
||||
"giveFeedback": "Оставить отзыв",
|
||||
"graphMode": "Графовый режим",
|
||||
"linearMode": "Режим приложения",
|
||||
"mobileControls": "Редактировать и запустить",
|
||||
"queue": {
|
||||
"clear": "Очистить очередь",
|
||||
"clickToClear": "Нажмите, чтобы очистить очередь"
|
||||
@@ -1383,7 +1373,6 @@
|
||||
"rerun": "Перезапустить",
|
||||
"reuseParameters": "Повторно использовать параметры",
|
||||
"runCount": "Количество запусков:",
|
||||
"viewJob": "Просмотреть задачу",
|
||||
"welcome": {
|
||||
"backToWorkflow": "Назад к рабочему процессу",
|
||||
"buildApp": "Создать приложение",
|
||||
@@ -1760,7 +1749,6 @@
|
||||
"disabled": "Отключено",
|
||||
"disabledTooltip": "Рабочий процесс не будет автоматически помещён в очередь",
|
||||
"execute": "Выполнить",
|
||||
"fullscreen": "Полноэкранный режим",
|
||||
"help": "Справка",
|
||||
"helpAndFeedback": "Помощь и обратная связь",
|
||||
"hideMenu": "Скрыть меню",
|
||||
@@ -1908,8 +1896,7 @@
|
||||
"Workflows": "Рабочие процессы",
|
||||
"Zoom In": "Увеличить",
|
||||
"Zoom Out": "Уменьшить",
|
||||
"Zoom to fit": "Масштабировать по размеру",
|
||||
"linearMode_appModeToolbar_apps": "linearMode.appModeToolbar.apps"
|
||||
"Zoom to fit": "Масштабировать по размеру"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Цвета узлов",
|
||||
@@ -3215,9 +3202,7 @@
|
||||
"enterFilename": "Введите название файла",
|
||||
"enterFilenamePrompt": "Введите имя файла:",
|
||||
"exportWorkflow": "Экспорт рабочего процесса",
|
||||
"saveWorkflow": "Сохранить рабочий процесс",
|
||||
"savedAsApp": "Преобразовано в рабочий процесс приложения",
|
||||
"savedAsWorkflow": "Преобразовано только в рабочий процесс графа узлов"
|
||||
"saveWorkflow": "Сохранить рабочий процесс"
|
||||
},
|
||||
"workspace": {
|
||||
"addedToWorkspace": "Вы были добавлены в {workspaceName}",
|
||||
|
||||
@@ -344,10 +344,6 @@
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "Odak Modunu Aç/Kapat"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_apps": {
|
||||
"label": "Uygulamalar Kenar Çubuğunu Aç/Kapat",
|
||||
"tooltip": "Uygulamalar"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "Varlıklar Kenar Çubuğunu Aç/Kapat",
|
||||
"tooltip": "Varlıklar"
|
||||
|
||||
@@ -326,40 +326,35 @@
|
||||
"deleteWorkflow": "İş Akışını Sil",
|
||||
"duplicate": "Çoğalt",
|
||||
"enterAppMode": "Uygulama moduna gir",
|
||||
"enterBuilderMode": "Uygulama oluşturucuya gir",
|
||||
"enterNewName": "Yeni isim girin",
|
||||
"exitAppMode": "Uygulama modundan çık",
|
||||
"missingNodesWarning": "İş akışında desteklenmeyen düğümler var (kırmızı ile vurgulanmış).",
|
||||
"workflowActions": "İş akışı işlemleri"
|
||||
},
|
||||
"builderMenu": {
|
||||
"exitAppBuilder": "Uygulama oluşturucudan çık"
|
||||
"exitAppBuilder": "Uygulama oluşturucudan çık",
|
||||
"saveApp": "Uygulamayı kaydet"
|
||||
},
|
||||
"builderToolbar": {
|
||||
"app": "Uygulama",
|
||||
"appDescription": "Varsayılan olarak uygulama olarak açılır",
|
||||
"arrange": "Önizleme",
|
||||
"arrangeDescription": "Uygulama düzenini gözden geçir",
|
||||
"backToWorkflow": "İş akışına geri dön",
|
||||
"connectOutput": "Bir çıktı bağla",
|
||||
"connectOutputBody1": "Uygulamanızın kaydedilebilmesi için en az bir çıktı bağlanmalıdır.",
|
||||
"connectOutputBody2": "'Seç' adımına geçin ve çıktı düğümlerine tıklayarak buraya ekleyin.",
|
||||
"defaultModeAppliedAppBody": "Bu iş akışı artık varsayılan olarak Uygulama Modunda açılacak.",
|
||||
"defaultModeAppliedAppPrompt": "Şimdi görüntülemek ister misiniz?",
|
||||
"defaultModeAppliedGraphBody": "Bu iş akışı artık varsayılan olarak bir düğüm grafiği olarak açılacak.",
|
||||
"defaultModeAppliedGraphPrompt": "Uygulamayı yine de görüntülemek ister misiniz?",
|
||||
"defaultModeAppliedTitle": "Başarıyla ayarlandı",
|
||||
"defaultView": "Varsayılan görünümü ayarla",
|
||||
"defaultViewDescription": "Nasıl açılacağını seçin",
|
||||
"defaultViewLabel": "Varsayılan olarak, bu iş akışı şu şekilde açılacak:",
|
||||
"defaultViewTitle": "Bu iş akışı için varsayılan görünümü ayarla",
|
||||
"emptyWorkflowExplanation": "İş akışınız boş. Bir uygulama oluşturmaya başlamak için önce bazı düğümlere ihtiyacınız var.",
|
||||
"emptyWorkflowPrompt": "Bir şablonla başlamak ister misiniz?",
|
||||
"emptyWorkflowTitle": "Bu iş akışında düğüm yok",
|
||||
"filename": "Dosya adı",
|
||||
"label": "Uygulama Oluşturucu",
|
||||
"loadTemplate": "Şablon yükle",
|
||||
"nodeGraph": "Düğüm grafiği",
|
||||
"nodeGraphDescription": "Varsayılan olarak düğüm grafiği olarak açılır",
|
||||
"save": "Kaydet",
|
||||
"saveAs": "Farklı kaydet",
|
||||
"saveAsLabel": "Bu iş akışını farklı kaydet ...",
|
||||
"saveDescription": "Kaydet ve bitir",
|
||||
"saveSuccess": "Başarıyla kaydedildi",
|
||||
"saveSuccessAppMessage": "'{name}' kaydedildi. Artık varsayılan olarak Uygulama Modunda açılacak.",
|
||||
"saveSuccessAppPrompt": "Şimdi görüntülemek ister misiniz?",
|
||||
"saveSuccessGraphMessage": "'{name}' kaydedildi. Varsayılan olarak düğüm grafiği olarak açılacak.",
|
||||
"select": "Seç",
|
||||
"selectDescription": "Girdi/çıktı seçin",
|
||||
"switchToSelect": "Seç'e Geç",
|
||||
@@ -1339,9 +1334,7 @@
|
||||
"linearMode": {
|
||||
"appModeToolbar": {
|
||||
"appBuilder": "Uygulama oluşturucu",
|
||||
"apps": "Uygulamalar",
|
||||
"appsEmptyMessage": "Kaydedilen uygulamalar burada görünecek.\nİlk uygulamanızı oluşturmak için aşağıya tıklayın.",
|
||||
"enterAppMode": "Uygulama moduna gir"
|
||||
"apps": "Uygulamalar"
|
||||
},
|
||||
"arrange": {
|
||||
"atLeastOne": "en az bir",
|
||||
@@ -1366,16 +1359,13 @@
|
||||
"outputsExample": "Örnekler: “Resmi Kaydet” veya “Videoyu Kaydet”",
|
||||
"promptAddInputs": "Girdi olarak eklemek için düğüm parametrelerine tıklayın",
|
||||
"promptAddOutputs": "Çıktı olarak eklemek için çıktı düğümlerine tıklayın. Bunlar oluşturulan sonuçlar olacak.",
|
||||
"title": "Uygulama oluşturucu modu",
|
||||
"unknownWidget": "Widget görünür değil"
|
||||
"title": "Uygulama oluşturucu modu"
|
||||
},
|
||||
"downloadAll": "Tümünü İndir",
|
||||
"dragAndDropImage": "Bir görseli sürükleyip bırakın",
|
||||
"enterNodeGraph": "Düğüm grafiğine gir",
|
||||
"giveFeedback": "Geri bildirim ver",
|
||||
"graphMode": "Grafik Modu",
|
||||
"linearMode": "Uygulama Modu",
|
||||
"mobileControls": "Düzenle ve Çalıştır",
|
||||
"queue": {
|
||||
"clear": "Kuyruğu temizle",
|
||||
"clickToClear": "Kuyruğu temizlemek için tıklayın"
|
||||
@@ -1383,7 +1373,6 @@
|
||||
"rerun": "Tekrar Çalıştır",
|
||||
"reuseParameters": "Parametreleri Yeniden Kullan",
|
||||
"runCount": "Çalıştırma sayısı:",
|
||||
"viewJob": "İşi Görüntüle",
|
||||
"welcome": {
|
||||
"backToWorkflow": "İş akışına geri dön",
|
||||
"buildApp": "Uygulama oluştur",
|
||||
@@ -1760,7 +1749,6 @@
|
||||
"disabled": "Devre Dışı",
|
||||
"disabledTooltip": "İş akışı otomatik olarak kuyruğa alınmayacak",
|
||||
"execute": "Yürüt",
|
||||
"fullscreen": "Tam ekran",
|
||||
"help": "Yardım",
|
||||
"helpAndFeedback": "Yardım ve Geri Bildirim",
|
||||
"hideMenu": "Menüyü Gizle",
|
||||
@@ -1908,8 +1896,7 @@
|
||||
"Workflows": "İş Akışları",
|
||||
"Zoom In": "Yakınlaştır",
|
||||
"Zoom Out": "Uzaklaştır",
|
||||
"Zoom to fit": "Sığdırmak için yakınlaştır",
|
||||
"linearMode_appModeToolbar_apps": "linearMode.appModeToolbar.apps"
|
||||
"Zoom to fit": "Sığdırmak için yakınlaştır"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Düğüm Renkleri",
|
||||
@@ -3215,9 +3202,7 @@
|
||||
"enterFilename": "Dosya adını girin",
|
||||
"enterFilenamePrompt": "Dosya adını girin:",
|
||||
"exportWorkflow": "İş Akışını Dışa Aktar",
|
||||
"saveWorkflow": "İş akışını kaydet",
|
||||
"savedAsApp": "Uygulama iş akışına dönüştürüldü",
|
||||
"savedAsWorkflow": "Yalnızca düğüm grafiği iş akışına dönüştürüldü"
|
||||
"saveWorkflow": "İş akışını kaydet"
|
||||
},
|
||||
"workspace": {
|
||||
"addedToWorkspace": "{workspaceName} çalışma alanına eklendiniz",
|
||||
|
||||
@@ -344,10 +344,6 @@
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "切換專注模式"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_apps": {
|
||||
"label": "切換應用程式側邊欄",
|
||||
"tooltip": "應用程式"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "切換資源側邊欄",
|
||||
"tooltip": "資源"
|
||||
|
||||
@@ -326,44 +326,39 @@
|
||||
"deleteWorkflow": "刪除工作流程",
|
||||
"duplicate": "複製",
|
||||
"enterAppMode": "進入應用模式",
|
||||
"enterBuilderMode": "進入應用程式建構器",
|
||||
"enterNewName": "輸入新名稱",
|
||||
"exitAppMode": "離開應用模式",
|
||||
"missingNodesWarning": "工作流程包含不支援的節點(以紅色標示)。",
|
||||
"workflowActions": "工作流程操作"
|
||||
},
|
||||
"builderMenu": {
|
||||
"exitAppBuilder": "離開應用程式建構器"
|
||||
"exitAppBuilder": "離開應用程式建構器",
|
||||
"saveApp": "儲存應用程式"
|
||||
},
|
||||
"builderToolbar": {
|
||||
"app": "應用",
|
||||
"appDescription": "預設以應用模式開啟",
|
||||
"arrange": "預覽",
|
||||
"arrangeDescription": "檢視應用版面",
|
||||
"backToWorkflow": "返回工作流程",
|
||||
"connectOutput": "連接輸出",
|
||||
"connectOutputBody1": "您的應用必須至少連接一個輸出才能儲存。",
|
||||
"connectOutputBody2": "切換到「選擇」步驟,點擊輸出節點以新增到這裡。",
|
||||
"defaultModeAppliedAppBody": "此工作流程將從現在起預設以應用程式模式開啟。",
|
||||
"defaultModeAppliedAppPrompt": "您想現在查看嗎?",
|
||||
"defaultModeAppliedGraphBody": "此工作流程將從現在起預設以節點圖形模式開啟。",
|
||||
"defaultModeAppliedGraphPrompt": "您還想查看應用程式嗎?",
|
||||
"defaultModeAppliedTitle": "設定成功",
|
||||
"defaultView": "設定預設檢視",
|
||||
"defaultViewDescription": "選擇開啟方式",
|
||||
"defaultViewLabel": "預設情況下,此工作流程將以以下方式開啟:",
|
||||
"defaultViewTitle": "設定此工作流程的預設檢視",
|
||||
"emptyWorkflowExplanation": "您的工作流程是空的。您需要先新增一些節點才能開始建立應用程式。",
|
||||
"emptyWorkflowPrompt": "您想從範本開始嗎?",
|
||||
"emptyWorkflowTitle": "此工作流程沒有節點",
|
||||
"filename": "檔案名稱",
|
||||
"label": "應用建立器",
|
||||
"loadTemplate": "載入範本",
|
||||
"nodeGraph": "節點圖",
|
||||
"nodeGraphDescription": "預設以節點圖開啟",
|
||||
"save": "儲存",
|
||||
"saveAs": "另存新檔",
|
||||
"saveAsLabel": "將此工作流程另存為...",
|
||||
"saveDescription": "儲存並完成",
|
||||
"saveSuccess": "儲存成功",
|
||||
"saveSuccessAppMessage": "「{name}」已儲存。從現在起將預設以應用模式開啟。",
|
||||
"saveSuccessAppPrompt": "您想現在檢視嗎?",
|
||||
"saveSuccessGraphMessage": "「{name}」已儲存。將預設以節點圖開啟。",
|
||||
"select": "選擇",
|
||||
"selectDescription": "選擇輸入/輸出",
|
||||
"switchToSelect": "切換到選擇",
|
||||
"viewApp": "查看應用程式"
|
||||
"viewApp": "檢視應用"
|
||||
},
|
||||
"clipboard": {
|
||||
"errorMessage": "複製到剪貼簿失敗",
|
||||
@@ -1339,9 +1334,7 @@
|
||||
"linearMode": {
|
||||
"appModeToolbar": {
|
||||
"appBuilder": "應用建立器",
|
||||
"apps": "應用",
|
||||
"appsEmptyMessage": "已儲存的應用程式將顯示在這裡。\n點擊下方開始建立您的第一個應用程式。",
|
||||
"enterAppMode": "進入應用程式模式"
|
||||
"apps": "應用"
|
||||
},
|
||||
"arrange": {
|
||||
"atLeastOne": "至少一個",
|
||||
@@ -1366,16 +1359,13 @@
|
||||
"outputsExample": "例如:「儲存圖像」或「儲存影片」",
|
||||
"promptAddInputs": "點擊節點參數,將其新增為輸入",
|
||||
"promptAddOutputs": "點擊輸出節點,將其新增於此。這些將是產生的結果。",
|
||||
"title": "應用程式建構模式",
|
||||
"unknownWidget": "元件不可見"
|
||||
"title": "應用程式建構模式"
|
||||
},
|
||||
"downloadAll": "全部下載",
|
||||
"dragAndDropImage": "拖曳圖片到此",
|
||||
"enterNodeGraph": "進入節點圖",
|
||||
"giveFeedback": "提供回饋",
|
||||
"graphMode": "圖形模式",
|
||||
"linearMode": "App 模式",
|
||||
"mobileControls": "編輯與執行",
|
||||
"queue": {
|
||||
"clear": "清除佇列",
|
||||
"clickToClear": "點擊以清除佇列"
|
||||
@@ -1383,7 +1373,6 @@
|
||||
"rerun": "重新執行",
|
||||
"reuseParameters": "重用參數",
|
||||
"runCount": "執行次數:",
|
||||
"viewJob": "檢視任務",
|
||||
"welcome": {
|
||||
"backToWorkflow": "返回工作流程",
|
||||
"buildApp": "建立應用",
|
||||
@@ -1760,7 +1749,6 @@
|
||||
"disabled": "已停用",
|
||||
"disabledTooltip": "工作流程將不會自動排入佇列",
|
||||
"execute": "執行",
|
||||
"fullscreen": "全螢幕",
|
||||
"help": "說明",
|
||||
"helpAndFeedback": "說明與回饋",
|
||||
"hideMenu": "隱藏選單",
|
||||
@@ -1908,8 +1896,7 @@
|
||||
"Workflows": "工作流程",
|
||||
"Zoom In": "放大",
|
||||
"Zoom Out": "縮小",
|
||||
"Zoom to fit": "縮放至適合大小",
|
||||
"linearMode_appModeToolbar_apps": "linearMode.appModeToolbar.apps"
|
||||
"Zoom to fit": "縮放至適合大小"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "節點顏色",
|
||||
@@ -3215,9 +3202,7 @@
|
||||
"enterFilename": "輸入檔案名稱",
|
||||
"enterFilenamePrompt": "請輸入檔案名稱:",
|
||||
"exportWorkflow": "匯出工作流程",
|
||||
"saveWorkflow": "儲存工作流程",
|
||||
"savedAsApp": "已轉換為應用程式工作流程",
|
||||
"savedAsWorkflow": "已轉換為僅節點圖工作流程"
|
||||
"saveWorkflow": "儲存工作流程"
|
||||
},
|
||||
"workspace": {
|
||||
"addedToWorkspace": "你已被加入 {workspaceName}",
|
||||
|
||||
@@ -344,10 +344,6 @@
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "切换焦点模式"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_apps": {
|
||||
"label": "切换应用侧边栏",
|
||||
"tooltip": "应用"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_assets": {
|
||||
"label": "切换资产侧边栏",
|
||||
"tooltip": "资产"
|
||||
|
||||
@@ -326,40 +326,35 @@
|
||||
"deleteWorkflow": "删除工作流",
|
||||
"duplicate": "复制",
|
||||
"enterAppMode": "进入应用模式",
|
||||
"enterBuilderMode": "进入应用构建器",
|
||||
"enterNewName": "输入新名称",
|
||||
"exitAppMode": "退出应用模式",
|
||||
"missingNodesWarning": "工作流包含不支持的节点(红色突出显示)。",
|
||||
"workflowActions": "工作流操作"
|
||||
},
|
||||
"builderMenu": {
|
||||
"exitAppBuilder": "退出应用构建器"
|
||||
"exitAppBuilder": "退出应用构建器",
|
||||
"saveApp": "保存应用"
|
||||
},
|
||||
"builderToolbar": {
|
||||
"app": "应用",
|
||||
"appDescription": "默认以应用方式打开",
|
||||
"arrange": "预览",
|
||||
"arrangeDescription": "查看应用布局",
|
||||
"backToWorkflow": "返回工作流",
|
||||
"connectOutput": "连接输出",
|
||||
"connectOutputBody1": "您的应用需要至少连接一个输出,才能保存。",
|
||||
"connectOutputBody2": "切换到“选择”步骤,点击输出节点将其添加到此处。",
|
||||
"defaultModeAppliedAppBody": "此工作流将从现在起默认以应用模式打开。",
|
||||
"defaultModeAppliedAppPrompt": "你想现在查看吗?",
|
||||
"defaultModeAppliedGraphBody": "此工作流将从现在起默认以节点图模式打开。",
|
||||
"defaultModeAppliedGraphPrompt": "你还想查看应用吗?",
|
||||
"defaultModeAppliedTitle": "设置成功",
|
||||
"defaultView": "设置默认视图",
|
||||
"defaultViewDescription": "选择打开方式",
|
||||
"defaultViewLabel": "默认情况下,此工作流将以以下方式打开:",
|
||||
"defaultViewTitle": "为此工作流设置默认视图",
|
||||
"emptyWorkflowExplanation": "你的工作流为空。你需要先添加一些节点来开始构建应用。",
|
||||
"emptyWorkflowPrompt": "要使用模板开始吗?",
|
||||
"emptyWorkflowTitle": "此工作流没有节点",
|
||||
"filename": "文件名",
|
||||
"label": "应用构建器",
|
||||
"loadTemplate": "加载模板",
|
||||
"nodeGraph": "节点图",
|
||||
"nodeGraphDescription": "默认以节点图方式打开",
|
||||
"save": "保存",
|
||||
"saveAs": "另存为",
|
||||
"saveAsLabel": "将此工作流另存为...",
|
||||
"saveDescription": "保存并完成",
|
||||
"saveSuccess": "保存成功",
|
||||
"saveSuccessAppMessage": "“{name}”已保存。今后将默认以应用模式打开。",
|
||||
"saveSuccessAppPrompt": "现在要查看吗?",
|
||||
"saveSuccessGraphMessage": "“{name}”已保存。今后将默认以节点图方式打开。",
|
||||
"select": "选择",
|
||||
"selectDescription": "选择输入/输出",
|
||||
"switchToSelect": "切换到选择",
|
||||
@@ -1339,9 +1334,7 @@
|
||||
"linearMode": {
|
||||
"appModeToolbar": {
|
||||
"appBuilder": "应用构建器",
|
||||
"apps": "应用",
|
||||
"appsEmptyMessage": "已保存的应用会显示在这里。\n点击下方开始构建你的第一个应用。",
|
||||
"enterAppMode": "进入应用模式"
|
||||
"apps": "应用"
|
||||
},
|
||||
"arrange": {
|
||||
"atLeastOne": "至少一个",
|
||||
@@ -1366,16 +1359,13 @@
|
||||
"outputsExample": "示例:“保存图像”或“保存视频”",
|
||||
"promptAddInputs": "点击节点参数,将其添加为输入项",
|
||||
"promptAddOutputs": "点击输出节点,将其添加到此处。这些将作为生成结果。",
|
||||
"title": "应用构建模式",
|
||||
"unknownWidget": "组件不可见"
|
||||
"title": "应用构建模式"
|
||||
},
|
||||
"downloadAll": "全部下载",
|
||||
"dragAndDropImage": "拖拽图片到此处",
|
||||
"enterNodeGraph": "进入节点图",
|
||||
"giveFeedback": "提供反馈",
|
||||
"graphMode": "图形模式",
|
||||
"linearMode": "App 模式",
|
||||
"mobileControls": "编辑与运行",
|
||||
"queue": {
|
||||
"clear": "清空队列",
|
||||
"clickToClear": "点击清空队列"
|
||||
@@ -1383,7 +1373,6 @@
|
||||
"rerun": "重新运行",
|
||||
"reuseParameters": "复用参数",
|
||||
"runCount": "运行次数:",
|
||||
"viewJob": "查看任务",
|
||||
"welcome": {
|
||||
"backToWorkflow": "返回工作流",
|
||||
"buildApp": "构建应用",
|
||||
@@ -1760,7 +1749,6 @@
|
||||
"disabled": "禁用",
|
||||
"disabledTooltip": "工作流将不会自动执行",
|
||||
"execute": "执行",
|
||||
"fullscreen": "全屏",
|
||||
"help": "说明",
|
||||
"helpAndFeedback": "帮助与反馈",
|
||||
"hideMenu": "隐藏菜单",
|
||||
@@ -1908,8 +1896,7 @@
|
||||
"Workflows": "工作流",
|
||||
"Zoom In": "放大画面",
|
||||
"Zoom Out": "缩小画面",
|
||||
"Zoom to fit": "缩放以适应",
|
||||
"linearMode_appModeToolbar_apps": "linearMode.appModeToolbar.apps"
|
||||
"Zoom to fit": "缩放以适应"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "节点颜色",
|
||||
@@ -3227,9 +3214,7 @@
|
||||
"enterFilename": "输入文件名",
|
||||
"enterFilenamePrompt": "输入文件名:",
|
||||
"exportWorkflow": "导出工作流",
|
||||
"saveWorkflow": "保存工作流",
|
||||
"savedAsApp": "已转换为应用工作流",
|
||||
"savedAsWorkflow": "已转换为节点图工作流"
|
||||
"saveWorkflow": "保存工作流"
|
||||
},
|
||||
"workspace": {
|
||||
"addedToWorkspace": "您已被加入 {workspaceName}",
|
||||
|
||||
@@ -12,7 +12,7 @@ import { app } from '@/scripts/app'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useModelToNodeStore } from '@/stores/modelToNodeStore'
|
||||
|
||||
interface ModelNodeCreateOptions {
|
||||
interface CreateNodeOptions {
|
||||
position?: Point
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ type Result<T, E> = { success: true; value: T } | { success: false; error: E }
|
||||
*/
|
||||
export function createModelNodeFromAsset(
|
||||
asset: AssetItem,
|
||||
options?: ModelNodeCreateOptions
|
||||
options?: CreateNodeOptions
|
||||
): Result<LGraphNode, NodeCreationError> {
|
||||
const validatedAsset = assetItemSchema.safeParse(asset)
|
||||
|
||||
|
||||
@@ -1207,13 +1207,6 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
defaultValue: false,
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Queue.ShowRunProgressBar',
|
||||
name: 'Show run progress bar',
|
||||
type: 'hidden',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.41.3'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Node.AlwaysShowAdvancedWidgets',
|
||||
category: ['LiteGraph', 'Node Widget', 'AlwaysShowAdvancedWidgets'],
|
||||
|
||||
@@ -56,7 +56,7 @@ const theButtonStyle = computed(() =>
|
||||
<div
|
||||
:class="
|
||||
cn(WidgetInputBaseClass, 'flex text-base leading-none', {
|
||||
'opacity-50 cursor-not-allowed outline-node-component-border': disabled
|
||||
'opacity-50 cursor-not-allowed outline-zinc-300/10': disabled
|
||||
})
|
||||
"
|
||||
>
|
||||
@@ -97,7 +97,7 @@ const theButtonStyle = computed(() =>
|
||||
cn(
|
||||
theButtonStyle,
|
||||
'relative',
|
||||
'size-8 flex justify-center items-center border-l rounded-r-lg border-node-component-border'
|
||||
'size-8 flex justify-center items-center border-l rounded-r-lg border-zinc-300/10'
|
||||
)
|
||||
"
|
||||
>
|
||||
|
||||
@@ -62,23 +62,13 @@ type LayoutConfig = {
|
||||
}
|
||||
|
||||
const LAYOUT_CONFIGS: Record<LayoutMode, LayoutConfig> = {
|
||||
grid: {
|
||||
maxColumns: 4,
|
||||
itemHeight: 120,
|
||||
itemWidth: 89,
|
||||
gap: 'var(--spacing-4) var(--spacing-2)'
|
||||
},
|
||||
list: {
|
||||
maxColumns: 1,
|
||||
itemHeight: 64,
|
||||
itemWidth: 380,
|
||||
gap: 'var(--spacing-2)'
|
||||
},
|
||||
grid: { maxColumns: 4, itemHeight: 120, itemWidth: 89, gap: '1rem 0.5rem' },
|
||||
list: { maxColumns: 1, itemHeight: 64, itemWidth: 380, gap: '0.5rem' },
|
||||
'list-small': {
|
||||
maxColumns: 1,
|
||||
itemHeight: 40,
|
||||
itemWidth: 380,
|
||||
gap: 'var(--spacing-1)'
|
||||
gap: '0.25rem'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -411,7 +411,6 @@ const zSettings = z.object({
|
||||
'Comfy.VueNodes.AutoScaleLayout': z.boolean(),
|
||||
'Comfy.Assets.UseAssetAPI': z.boolean(),
|
||||
'Comfy.Queue.QPOV2': z.boolean(),
|
||||
'Comfy.Queue.ShowRunProgressBar': z.boolean(),
|
||||
'Comfy-Desktop.AutoUpdate': z.boolean(),
|
||||
'Comfy-Desktop.SendStatistics': z.boolean(),
|
||||
'Comfy-Desktop.WindowStyle': z.string(),
|
||||
|
||||
@@ -66,7 +66,9 @@ function createMockCanvas(): Partial<LGraphCanvas> {
|
||||
|
||||
return {
|
||||
graph: mockGraph as LGraph,
|
||||
selectItems: vi.fn()
|
||||
selectItems: vi.fn(),
|
||||
emitBeforeChange: vi.fn(),
|
||||
emitAfterChange: vi.fn()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +93,10 @@ describe('ComfyApp', () => {
|
||||
const mockNode2 = createMockNode({ id: 2 })
|
||||
const mockBatchNode = createMockNode({ id: 3, type: 'BatchImagesNode' })
|
||||
|
||||
vi.mocked(pasteImageNodes).mockResolvedValue([mockNode1, mockNode2])
|
||||
vi.mocked(pasteImageNodes).mockResolvedValue({
|
||||
nodes: [mockNode1, mockNode2],
|
||||
completion: Promise.resolve()
|
||||
})
|
||||
vi.mocked(createNode).mockResolvedValue(mockBatchNode)
|
||||
|
||||
const file1 = createTestFile('test1.png', 'image/png')
|
||||
@@ -102,18 +107,21 @@ describe('ComfyApp', () => {
|
||||
|
||||
expect(pasteImageNodes).toHaveBeenCalledWith(mockCanvas, files)
|
||||
expect(createNode).toHaveBeenCalledWith(mockCanvas, 'BatchImagesNode')
|
||||
expect(mockNode1.connect).toHaveBeenCalledWith(0, mockBatchNode, 0)
|
||||
expect(mockNode2.connect).toHaveBeenCalledWith(0, mockBatchNode, 1)
|
||||
expect(mockCanvas.selectItems).toHaveBeenCalledWith([
|
||||
mockNode1,
|
||||
mockNode2,
|
||||
mockBatchNode
|
||||
])
|
||||
expect(mockNode1.connect).toHaveBeenCalledWith(0, mockBatchNode, 0)
|
||||
expect(mockNode2.connect).toHaveBeenCalledWith(0, mockBatchNode, 1)
|
||||
})
|
||||
|
||||
it('should select single image node without batch node', async () => {
|
||||
const mockNode1 = createMockNode({ id: 1 })
|
||||
vi.mocked(pasteImageNodes).mockResolvedValue([mockNode1])
|
||||
vi.mocked(pasteImageNodes).mockResolvedValue({
|
||||
nodes: [mockNode1],
|
||||
completion: Promise.resolve()
|
||||
})
|
||||
|
||||
const file = createTestFile('test.png', 'image/png')
|
||||
|
||||
@@ -199,7 +207,7 @@ describe('ComfyApp', () => {
|
||||
it('should position batch node to the right of first node', () => {
|
||||
const mockNode1 = createMockNode({
|
||||
pos: [100, 200],
|
||||
getBounding: vi.fn(() => new Float64Array([100, 200, 300, 400]))
|
||||
size: [300, 400]
|
||||
})
|
||||
const mockBatchNode = createMockNode({ pos: [0, 0] })
|
||||
|
||||
@@ -211,8 +219,8 @@ describe('ComfyApp', () => {
|
||||
it('should stack multiple image nodes vertically', () => {
|
||||
const mockNode1 = createMockNode({
|
||||
pos: [100, 200],
|
||||
type: 'LoadImage',
|
||||
getBounding: vi.fn(() => new Float64Array([100, 200, 300, 400]))
|
||||
size: [300, 400],
|
||||
type: 'LoadImage'
|
||||
})
|
||||
const mockNode2 = createMockNode({ pos: [0, 0], type: 'LoadImage' })
|
||||
const mockNode3 = createMockNode({ pos: [0, 0], type: 'LoadImage' })
|
||||
@@ -227,7 +235,8 @@ describe('ComfyApp', () => {
|
||||
|
||||
it('should call graph change once for all nodes', () => {
|
||||
const mockNode1 = createMockNode({
|
||||
getBounding: vi.fn(() => new Float64Array([100, 200, 300, 400]))
|
||||
pos: [100, 200],
|
||||
size: [300, 400]
|
||||
})
|
||||
const mockBatchNode = createMockNode()
|
||||
|
||||
|
||||
@@ -1720,21 +1720,28 @@ export class ComfyApp {
|
||||
if (fileList.length === 0) return
|
||||
if (!fileList[0].type.startsWith('image')) return
|
||||
|
||||
const imageNodes = await pasteImageNodes(this.canvas, fileList)
|
||||
if (imageNodes.length === 0) return
|
||||
this.canvas.emitBeforeChange()
|
||||
try {
|
||||
const { nodes, completion } = await pasteImageNodes(this.canvas, fileList)
|
||||
if (nodes.length === 0) return
|
||||
|
||||
if (imageNodes.length > 1) {
|
||||
const batchImagesNode = await createNode(this.canvas, 'BatchImagesNode')
|
||||
if (!batchImagesNode) return
|
||||
if (nodes.length > 1) {
|
||||
const batchImagesNode = await createNode(this.canvas, 'BatchImagesNode')
|
||||
if (!batchImagesNode) return
|
||||
|
||||
this.positionBatchNodes(imageNodes, batchImagesNode)
|
||||
this.canvas.selectItems([...imageNodes, batchImagesNode])
|
||||
this.positionBatchNodes(nodes, batchImagesNode)
|
||||
this.canvas.selectItems([...nodes, batchImagesNode])
|
||||
|
||||
imageNodes.forEach((imageNode, index) => {
|
||||
imageNode.connect(0, batchImagesNode, index)
|
||||
})
|
||||
} else {
|
||||
this.canvas.selectItems(imageNodes)
|
||||
nodes.forEach((imageNode, index) => {
|
||||
imageNode.connect(0, batchImagesNode, index)
|
||||
})
|
||||
} else {
|
||||
this.canvas.selectItems(nodes)
|
||||
}
|
||||
|
||||
await completion
|
||||
} finally {
|
||||
this.canvas.emitAfterChange()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1775,8 +1782,9 @@ export class ComfyApp {
|
||||
}
|
||||
|
||||
positionBatchNodes(nodes: LGraphNode[], batchNode: LGraphNode): void {
|
||||
const [x, y, width] = nodes[0].getBounding()
|
||||
batchNode.pos = [x + width + 100, y + 30]
|
||||
const [x, y] = nodes[0].pos
|
||||
const nodeWidth = nodes[0].size[0]
|
||||
batchNode.pos = [x + nodeWidth + 100, y + 30]
|
||||
|
||||
// Retrieving Node Height is inconsistent
|
||||
let height = 0
|
||||
|
||||
66
src/scripts/changeTracker.test.ts
Normal file
66
src/scripts/changeTracker.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
|
||||
import { ChangeTracker } from './changeTracker'
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
graph: {},
|
||||
rootGraph: { serialize: () => ({ nodes: [], links: [] }) },
|
||||
canvas: { ds: { scale: 1, offset: [0, 0] } }
|
||||
}
|
||||
}))
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
dispatchCustomEvent: vi.fn(),
|
||||
apiURL: vi.fn((path: string) => path)
|
||||
}
|
||||
}))
|
||||
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||
useWorkflowStore: () => ({ getWorkflowByPath: () => null })
|
||||
}))
|
||||
vi.mock('@/stores/subgraphNavigationStore', () => ({
|
||||
useSubgraphNavigationStore: () => ({ exportState: () => [] })
|
||||
}))
|
||||
|
||||
describe('ChangeTracker', () => {
|
||||
let tracker: ChangeTracker
|
||||
|
||||
beforeEach(() => {
|
||||
const mockWorkflow = { path: 'test.json' } as unknown as ComfyWorkflow
|
||||
const initialState = {
|
||||
version: 1,
|
||||
nodes: [],
|
||||
links: [],
|
||||
last_node_id: 0,
|
||||
last_link_id: 0
|
||||
} as unknown as ComfyWorkflowJSON
|
||||
tracker = new ChangeTracker(mockWorkflow, initialState)
|
||||
})
|
||||
|
||||
describe('beforeChange / afterChange batching', () => {
|
||||
it('calls checkState only when outermost afterChange completes', () => {
|
||||
const checkStateSpy = vi.spyOn(tracker, 'checkState')
|
||||
|
||||
tracker.beforeChange()
|
||||
tracker.afterChange()
|
||||
|
||||
expect(checkStateSpy).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('suppresses checkState for nested calls until fully unwound', () => {
|
||||
const checkStateSpy = vi.spyOn(tracker, 'checkState')
|
||||
|
||||
tracker.beforeChange()
|
||||
tracker.beforeChange()
|
||||
|
||||
tracker.afterChange()
|
||||
expect(checkStateSpy).not.toHaveBeenCalled()
|
||||
|
||||
tracker.afterChange()
|
||||
expect(checkStateSpy).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -34,6 +34,13 @@ export class ChangeTracker {
|
||||
activeState: ComfyWorkflowJSON
|
||||
undoQueue: ComfyWorkflowJSON[] = []
|
||||
redoQueue: ComfyWorkflowJSON[] = []
|
||||
/**
|
||||
* Nesting counter for compound operations. While greater than zero,
|
||||
* {@link checkState} is suppressed. Incremented by {@link beforeChange},
|
||||
* decremented by {@link afterChange}. When it returns to zero,
|
||||
* `checkState()` runs and captures a single undo entry for all mutations
|
||||
* since the first `beforeChange`.
|
||||
*/
|
||||
changeCount: number = 0
|
||||
/**
|
||||
* Whether the redo/undo restoring is in progress.
|
||||
@@ -203,10 +210,24 @@ export class ChangeTracker {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the start of a compound operation. Increments the nesting
|
||||
* counter to suppress {@link checkState} until the matching
|
||||
* {@link afterChange} call completes.
|
||||
*
|
||||
* Typically called via `LGraphCanvas.emitBeforeChange()` through the
|
||||
* `litegraph:canvas` DOM event, rather than directly.
|
||||
*/
|
||||
beforeChange() {
|
||||
this.changeCount++
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the end of a compound operation. Decrements the nesting
|
||||
* counter; when it reaches zero, calls {@link checkState} to capture
|
||||
* a single undo entry for all mutations since the first
|
||||
* {@link beforeChange}.
|
||||
*/
|
||||
afterChange() {
|
||||
if (!--this.changeCount) {
|
||||
this.checkState()
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
createBounds
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
CreateNodeOptions,
|
||||
GraphAddOptions,
|
||||
IContextMenuValue,
|
||||
Point,
|
||||
@@ -886,7 +885,7 @@ export const useLitegraphService = () => {
|
||||
|
||||
function addNodeOnGraph(
|
||||
nodeDef: ComfyNodeDefV1 | ComfyNodeDefV2,
|
||||
options: CreateNodeOptions = {},
|
||||
options: Record<string, unknown> & { pos?: Point } = {},
|
||||
addOptions?: GraphAddOptions
|
||||
): LGraphNode | null {
|
||||
options.pos ??= getCanvasCenter()
|
||||
|
||||
2
src/types/litegraph-augmentation.d.ts
vendored
2
src/types/litegraph-augmentation.d.ts
vendored
@@ -200,7 +200,7 @@ declare module '@/lib/litegraph/src/litegraph' {
|
||||
/** Callback for pasting an image file into the node */
|
||||
pasteFile?(file: File): void
|
||||
/** Callback for pasting multiple files into the node */
|
||||
pasteFiles?(files: File[]): void
|
||||
pasteFiles?(files: File[]): boolean | Promise<boolean>
|
||||
}
|
||||
/**
|
||||
* Only used by the Primitive node. Primitive node is using the widget property
|
||||
|
||||
@@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import {
|
||||
compareExecutionId,
|
||||
createNodeExecutionId,
|
||||
createNodeLocatorId,
|
||||
getAncestorExecutionIds,
|
||||
@@ -233,45 +232,4 @@ describe('nodeIdentification', () => {
|
||||
expect(getParentExecutionIds('65:70:63')).toEqual(['65', '65:70'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('compareExecutionId', () => {
|
||||
it('sorts simple numeric IDs in ascending order', () => {
|
||||
expect(compareExecutionId('1', '2')).toBeLessThan(0)
|
||||
expect(compareExecutionId('2', '1')).toBeGreaterThan(0)
|
||||
expect(compareExecutionId('5', '5')).toBe(0)
|
||||
})
|
||||
|
||||
it('compares nested IDs left-to-right by segment', () => {
|
||||
// "1" < "1:20" < "2" < "10:11:12" as documented
|
||||
expect(compareExecutionId('1', '1:20')).toBeLessThan(0)
|
||||
expect(compareExecutionId('1:20', '2')).toBeLessThan(0)
|
||||
expect(compareExecutionId('2', '10:11:12')).toBeLessThan(0)
|
||||
})
|
||||
|
||||
it('treats a shorter ID as having trailing segment 0 when comparing', () => {
|
||||
// "5" vs "5:0" → first segments equal, second: 0 vs 0 → equal
|
||||
expect(compareExecutionId('5', '5:0')).toBe(0)
|
||||
// "5" vs "5:1" → second segment 0 < 1
|
||||
expect(compareExecutionId('5', '5:1')).toBeLessThan(0)
|
||||
})
|
||||
|
||||
it('handles undefined inputs by treating them as empty string (segment 0)', () => {
|
||||
expect(compareExecutionId(undefined, '1')).toBeLessThan(0)
|
||||
expect(compareExecutionId('1', undefined)).toBeGreaterThan(0)
|
||||
expect(compareExecutionId(undefined, undefined)).toBe(0)
|
||||
})
|
||||
|
||||
it('handles empty string inputs', () => {
|
||||
expect(compareExecutionId('', '1')).toBeLessThan(0)
|
||||
expect(compareExecutionId('1', '')).toBeGreaterThan(0)
|
||||
expect(compareExecutionId('', '')).toBe(0)
|
||||
})
|
||||
|
||||
it('treats non-numeric segments as 0 via NaN guard', () => {
|
||||
// Number('foo') → NaN → treated as 0
|
||||
expect(compareExecutionId('foo', '1')).toBeLessThan(0)
|
||||
expect(compareExecutionId('foo', '0')).toBe(0)
|
||||
expect(compareExecutionId('1:foo', '1:0')).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -146,26 +146,3 @@ export function getParentExecutionIds(
|
||||
): NodeExecutionId[] {
|
||||
return getAncestorExecutionIds(executionId).slice(0, -1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two NodeExecutionIds for ascending numeric sort order.
|
||||
* Splits each ID by ":" and compares segments numerically left to right.
|
||||
*
|
||||
* Example order: "1" < "1:20" < "2" < "10:11:12"
|
||||
*/
|
||||
export function compareExecutionId(
|
||||
a: string | undefined,
|
||||
b: string | undefined
|
||||
): number {
|
||||
const parse = (id: string | undefined) => (id ?? '').split(':').map(Number)
|
||||
const idA = parse(a)
|
||||
const idB = parse(b)
|
||||
for (let i = 0; i < Math.max(idA.length, idB.length); i++) {
|
||||
const segA = idA[i] ?? 0
|
||||
const segB = idB[i] ?? 0
|
||||
const diff =
|
||||
(Number.isNaN(segA) ? 0 : segA) - (Number.isNaN(segB) ? 0 : segB)
|
||||
if (diff !== 0) return diff
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user