Compare commits

...

5 Commits

Author SHA1 Message Date
Alexis Rolland
a16d488838 Merge branch 'main' into DynamicGroupSupport 2026-06-26 17:09:45 +08:00
AustinMroz
13b42d9b59 Ensure dynamic combo children cleanup state (#13073)
#12617 introduced a regression in Dynamic Combos. If two options have
child widgets of the same name (such as `bit_depth` on `Save Image
(Advanced)`), then widget state would be incorrectly shared between the
two widgets.

This is resolved by having removed widgets also delete their state.

There was previous interest in having widgets of this type keep state
when valid. This interest remains, but will require a more controlled
intentional implementation in the future.

Since the bit depth options on `Save Image (Advanced)` could potentially
be expanded in the future, this PR specifically adds a new devtools node
for testing with.

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-06-26 01:08:06 +00:00
Comfy Org PR Bot
e604c85b88 1.47.5 (#13166)
Patch version increment to 1.47.5

**Base branch:** `main`

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-06-26 00:51:57 +00:00
AustinMroz
7ae3ad936c When dragging vue nodes, also drag reroutes (#12885)
`selectedItems` was being filtered to nodes and groups. Since no special
behaviour is being performed on groups, the 'move groups' code is
relaxed to instead 'move all non-node selected items'.
2026-06-26 00:11:48 +00:00
Talmaj Marinc
842e3d7541 Initial commit for DynamiGroupSupport. 2026-06-25 00:14:28 +02:00
26 changed files with 977 additions and 51 deletions

View File

@@ -6,6 +6,7 @@ import {
} from '@e2e/fixtures/ComfyPage'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import type { Position } from '@e2e/fixtures/types'
import { VueNodeFixture } from '@e2e/fixtures/utils/vueNodeFixtures'
test.describe('Vue Node Moving', { tag: '@vue-nodes' }, () => {
const getHeaderPos = async (
@@ -359,6 +360,55 @@ test.describe('Vue Node Moving', { tag: '@vue-nodes' }, () => {
expect(await getOffset(), 'drag canceled').toEqual(secondaryOffset)
})
test('dragging a node moves all selected items', async ({
comfyPage,
comfyMouse
}) => {
const samplerLocator = comfyPage.vueNodes.getNodeByTitle('KSampler')
const ksampler = new VueNodeFixture(samplerLocator)
const loaderLocator = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
const loader = new VueNodeFixture(loaderLocator)
await test.step('create graph with group and reroute', async () => {
await comfyPage.nodeOps.clearGraph()
await comfyPage.searchBoxV2.addNode('Load Checkpoint')
const samplerOptions = { position: { x: 800, y: 200 } }
await comfyPage.searchBoxV2.addNode('KSampler', samplerOptions)
await ksampler.getSlot('model').dragTo(loader.getSlot('MODEL'))
await test.step('add reroute', async () => {
const b1 = await ksampler.getSlot('model').boundingBox()
const b2 = await loader.getSlot('MODEL').boundingBox()
if (!b1 || !b2) throw new Error('Failed to get bounds')
const x = (b1.x + b2.x + (b1.width + b2.width) / 2) / 2
const y = (b1.y + b2.y + (b1.height + b2.height) / 2) / 2
await comfyPage.page.keyboard.down('Alt')
await comfyPage.page.mouse.click(x, y)
await comfyPage.page.keyboard.up('Alt')
const rerouteCount = () =>
comfyPage.page.evaluate(() => graph!.reroutes.size)
await expect.poll(rerouteCount).toBe(1)
})
await comfyPage.keyboard.selectAll()
await comfyPage.page.keyboard.press('Control+G')
await comfyPage.keyboard.selectAll()
})
const getReroutePos = () =>
comfyPage.page.evaluate(() => [...graph!.reroutes.values()][0])
const getGroupPos = () =>
comfyPage.page.evaluate(() => graph!.groups[0].pos)
const initialReroutePos = await getReroutePos()
const initialGroupPos = await getGroupPos()
await comfyMouse.dragElementBy(ksampler.title, { x: 100 })
await expect.poll(getReroutePos).not.toEqual(initialReroutePos)
await expect.poll(getGroupPos).not.toEqual(initialGroupPos)
})
test(
'@mobile should allow moving nodes by dragging on touch devices',
{ tag: '@screenshot' },

View File

@@ -73,4 +73,16 @@ test.describe('Vue Widget Reactivity', { tag: '@vue-nodes' }, () => {
await expect(widget, 'Widget has restored value').toHaveText('scale width')
})
test('Dynamic children have separate state', async ({ comfyPage }) => {
const nodeName = 'Node With Dynamic Combo'
await comfyPage.searchBoxV2.addNode(nodeName, {
position: { x: 200, y: 150 }
})
const child = comfyPage.vueNodes.getWidgetByName(nodeName, 'suboption')
await expect(child, 'initial state').toHaveText('1x')
await comfyPage.vueNodes.selectComboOption(nodeName, 'combo', 'option2')
await expect(child, 'child of same name has new state').toHaveText('2x')
})
})

View File

@@ -1,6 +1,6 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.47.4",
"version": "1.47.5",
"private": true,
"description": "Official front-end implementation of ComfyUI",
"homepage": "https://comfy.org",

View File

@@ -344,6 +344,15 @@ export const zDynamicComboInputSpec = z.tuple([
})
])
export const zDynamicGroupInputSpec = z.tuple([
z.literal('COMFY_DYNAMICGROUP_V3'),
zBaseInputOptions.extend({
template: zComfyInputsSpec,
min: z.number().int().nonnegative().optional().default(0),
max: z.number().int().positive().max(100).optional().default(50)
})
])
export const zMatchTypeOptions = z.object({
...zBaseInputOptions.shape,
type: z.literal('COMFY_MATCHTYPE_V3'),

View File

@@ -84,6 +84,7 @@ export interface SafeWidgetData {
advanced?: boolean
hidden?: boolean
read_only?: boolean
removable?: boolean
values?: unknown
}
/** Input specification from node definition */
@@ -213,7 +214,8 @@ function extractWidgetDisplayOptions(
canvasOnly: widget.options.canvasOnly,
advanced: widget.options?.advanced ?? widget.advanced,
hidden: widget.options.hidden,
read_only: widget.options.read_only
read_only: widget.options.read_only,
removable: widget.options.removable
}
}

View File

@@ -1,5 +1,9 @@
import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration'
import { zAutogrowOptions, zMatchTypeOptions } from '@/schemas/nodeDefSchema'
import {
zAutogrowOptions,
zDynamicGroupInputSpec,
zMatchTypeOptions
} from '@/schemas/nodeDefSchema'
import type { InputSpec } from '@/schemas/nodeDefSchema'
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
@@ -8,6 +12,7 @@ const dynamicTypeResolvers: Record<
(inputSpec: InputSpecV2) => string[]
> = {
COMFY_AUTOGROW_V3: resolveAutogrowType,
COMFY_DYNAMICGROUP_V3: resolveDynamicGroupType,
COMFY_MATCHTYPE_V3: (input) =>
zMatchTypeOptions
.safeParse(input)
@@ -20,6 +25,21 @@ export function resolveInputType(input: InputSpecV2): string[] {
: input.type.split(',')
}
function resolveDynamicGroupType(rawSpec: InputSpecV2): string[] {
const parsed = zDynamicGroupInputSpec.safeParse([rawSpec.type, rawSpec])
const template = parsed.data?.[1]?.template
if (!template) return []
const inputTypes: (Record<string, InputSpec> | undefined)[] = [
template.required,
template.optional
]
return inputTypes.flatMap((inputType) =>
Object.entries(inputType ?? {}).flatMap(([name, v]) =>
resolveInputType(transformInputSpecV1ToV2(v, { name }))
)
)
}
function resolveAutogrowType(rawSpec: InputSpecV2): string[] {
const { input } = zAutogrowOptions.safeParse(rawSpec).data?.template ?? {}

View File

@@ -1,7 +1,9 @@
import { setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { describe, expect, test, vi } from 'vitest'
import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { LGraph, LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { Point } from '@/lib/litegraph/src/interfaces'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration'
import type { InputSpec } from '@/schemas/nodeDefSchema'
import { useLitegraphService } from '@/services/litegraphService'
@@ -47,6 +49,22 @@ function addDynamicCombo(node: LGraphNode, inputs: DynamicInputs) {
transformInputSpecV1ToV2(inputSpec, { name: namePrefix, isOptional: false })
)
}
function addDynamicGroup(
node: LGraphNode,
template: object,
{ min, max, name = 'g' }: { min?: number; max?: number; name?: string } = {}
) {
const options: Record<string, unknown> = { template }
if (min !== undefined) options.min = min
if (max !== undefined) options.max = max
addNodeInput(
node,
transformInputSpecV1ToV2(['COMFY_DYNAMICGROUP_V3', options] as InputSpec, {
name,
isOptional: false
})
)
}
function addAutogrow(node: LGraphNode, template: unknown) {
addNodeInput(
node,
@@ -287,3 +305,101 @@ describe('Autogrow', () => {
])
})
})
describe('Dynamic Groups', () => {
const stringTemplate = { required: { a: ['STRING', {}] } }
const widgetNames = (node: LGraphNode) => node.widgets!.map((w) => w.name)
const inputNames = (node: LGraphNode) => node.inputs.map((i) => i.name)
const widgetNamed = (node: LGraphNode, name: string) =>
node.widgets!.find((w) => w.name === name)!
test('renders min rows on creation', () => {
const node = testNode()
addDynamicGroup(node, stringTemplate, { min: 2, max: 5 })
expect(widgetNames(node)).toStrictEqual([
'g',
'g.__row__0',
'g.0.a',
'g.__row__1',
'g.1.a'
])
expect(inputNames(node)).toStrictEqual(['g.0.a', 'g.1.a'])
})
test('add row appends a new row up to max', () => {
const node = testNode()
addDynamicGroup(node, stringTemplate, { min: 0, max: 2 })
expect(widgetNames(node)).toStrictEqual(['g'])
widgetNamed(node, 'g').callback?.(undefined)
expect(inputNames(node)).toStrictEqual(['g.0.a'])
widgetNamed(node, 'g').callback?.(undefined)
expect(inputNames(node)).toStrictEqual(['g.0.a', 'g.1.a'])
// At max, further adds are ignored.
widgetNamed(node, 'g').callback?.(undefined)
expect(inputNames(node)).toStrictEqual(['g.0.a', 'g.1.a'])
})
test('remove row renumbers later rows', () => {
const node = testNode()
addDynamicGroup(node, stringTemplate, { min: 0, max: 5 })
widgetNamed(node, 'g').callback?.(undefined)
widgetNamed(node, 'g').callback?.(undefined)
widgetNamed(node, 'g').callback?.(undefined)
const row0Field = widgetNamed(node, 'g.0.a')
const row2Field = widgetNamed(node, 'g.2.a')
widgetNamed(node, 'g.__row__1').callback?.(undefined)
expect(widgetNames(node)).toStrictEqual([
'g',
'g.__row__0',
'g.0.a',
'g.__row__1',
'g.1.a'
])
expect(inputNames(node)).toStrictEqual(['g.0.a', 'g.1.a'])
// Row 0 is untouched; the former row 2 shifts down into row 1.
expect(widgetNamed(node, 'g.0.a')).toBe(row0Field)
expect(widgetNamed(node, 'g.1.a')).toBe(row2Field)
})
test('rows below min are not removable', () => {
const node = testNode()
addDynamicGroup(node, stringTemplate, { min: 1, max: 5 })
widgetNamed(node, 'g').callback?.(undefined)
expect(widgetNamed(node, 'g.__row__0').options?.removable).toBe(false)
expect(widgetNamed(node, 'g.__row__1').options?.removable).toBe(true)
// Attempting to remove a protected row is a no-op.
widgetNamed(node, 'g.__row__0').callback?.(undefined)
expect(inputNames(node)).toStrictEqual(['g.0.a', 'g.1.a'])
})
test('canvas click removes a row only on the remove hit target', () => {
const node = testNode()
addDynamicGroup(node, stringTemplate, { min: 0, max: 5 })
widgetNamed(node, 'g').callback?.(undefined)
widgetNamed(node, 'g').callback?.(undefined)
const header = widgetNamed(node, 'g.__row__1')
const up = { type: 'pointerup' } as CanvasPointerEvent
const down = { type: 'pointerdown' } as CanvasPointerEvent
const xCenter = node.size[0] - 15 - LiteGraph.NODE_WIDGET_HEIGHT * 0.5
// Releasing away from the remove target does nothing.
header.mouse?.(up, [0, 0] as Point, node)
expect(inputNames(node)).toStrictEqual(['g.0.a', 'g.1.a'])
// A pointerdown on the target does nothing (only release acts).
header.mouse?.(down, [xCenter, 0] as Point, node)
expect(inputNames(node)).toStrictEqual(['g.0.a', 'g.1.a'])
// Releasing on the target removes the row.
header.mouse?.(up, [xCenter, 0] as Point, node)
expect(inputNames(node)).toStrictEqual(['g.0.a'])
})
})

View File

@@ -2,10 +2,12 @@ import { remove } from 'es-toolkit'
import { shallowReactive } from 'vue'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import { t } from '@/i18n'
import type {
ISlotType,
INodeInputSlot,
INodeOutputSlot
INodeOutputSlot,
Point
} from '@/lib/litegraph/src/interfaces'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
@@ -13,11 +15,14 @@ import type { LLink } from '@/lib/litegraph/src/LLink'
import { commonType } from '@/lib/litegraph/src/utils/type'
import { resolveNodeRootGraphId } from '@/lib/litegraph/src/utils/widget'
import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import type { ComboInputSpec, InputSpec } from '@/schemas/nodeDefSchema'
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
import {
zAutogrowOptions,
zDynamicComboInputSpec,
zDynamicGroupInputSpec,
zMatchTypeOptions
} from '@/schemas/nodeDefSchema'
import { useLitegraphService } from '@/services/litegraphService'
@@ -28,6 +33,15 @@ import { widgetId } from '@/types/widgetId'
const INLINE_INPUTS = false
type DynamicGroupState = {
min: number
max: number
inputSpecs: InputSpecV2[]
}
type DynamicGroupNode = LGraphNode & {
comfyDynamic: { dynamicGroup: Record<string, DynamicGroupState> }
}
type MatchTypeNode = LGraphNode &
Pick<Required<LGraphNode>, 'onConnectionsChange'> & {
comfyDynamic: { matchType: Record<string, Record<string, string>> }
@@ -77,6 +91,7 @@ function dynamicComboWidget(
widgetName?: string
) {
const { addNodeInput } = useLitegraphService()
const { deleteWidget } = useWidgetValueStore()
const parseResult = zDynamicComboInputSpec.safeParse(untypedInputData)
if (!parseResult.success) throw new Error('invalid DynamicCombo spec')
const inputData = parseResult.data
@@ -99,7 +114,10 @@ function dynamicComboWidget(
const newSpec = value ? options[value] : undefined
const removedInputs = remove(node.inputs, isInGroup)
for (const widget of remove(node.widgets, isInGroup)) widget.onRemove?.()
for (const widget of remove(node.widgets, isInGroup)) {
widget.onRemove?.()
if (widget.widgetId) deleteWidget(widget.widgetId)
}
if (!newSpec) return
@@ -210,7 +228,321 @@ function dynamicComboWidget(
return { widget, minWidth, minHeight }
}
export const dynamicWidgets = { COMFY_DYNAMICCOMBO_V3: dynamicComboWidget }
function withComfyDynamicGroup(
node: LGraphNode
): asserts node is DynamicGroupNode {
if (node.comfyDynamic?.dynamicGroup) return
node.comfyDynamic ??= {}
node.comfyDynamic.dynamicGroup = {}
}
const ROW_MARKER = '__row__'
const rowHeaderName = (group: string, row: number) =>
`${group}.${ROW_MARKER}${row}`
const fieldName = (group: string, row: number, field: string) =>
`${group}.${row}.${field}`
/** Extract the row index from a header widget name, or `undefined`. */
function headerRowIndex(group: string, name: string): number | undefined {
const prefix = `${group}.${ROW_MARKER}`
if (!name.startsWith(prefix)) return undefined
const row = Number(name.slice(prefix.length))
return Number.isInteger(row) ? row : undefined
}
/** Rename a field that sits above the removed row, shifting its index down. */
function shiftedFieldName(
group: string,
name: string,
removedRow: number
): string | undefined {
const prefix = `${group}.`
if (!name.startsWith(prefix)) return undefined
const rest = name.slice(prefix.length)
const dot = rest.indexOf('.')
if (dot === -1) return undefined
const row = Number(rest.slice(0, dot))
if (!Number.isInteger(row) || row <= removedRow) return undefined
return fieldName(group, row - 1, rest.slice(dot + 1))
}
const belongsToRow = (group: string, name: string, row: number): boolean =>
name === rowHeaderName(group, row) || name.startsWith(`${group}.${row}.`)
const CANVAS_MARGIN = 15
/** Draw the "Add row" capsule button on the LiteGraph canvas. */
function drawGroupButton(
ctx: CanvasRenderingContext2D,
width: number,
y: number,
label: string,
disabled: boolean
): void {
const height = LiteGraph.NODE_WIDGET_HEIGHT
ctx.save()
if (disabled) ctx.globalAlpha *= 0.5
ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR
ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR
ctx.beginPath()
ctx.roundRect(CANVAS_MARGIN, y, width - CANVAS_MARGIN * 2, height, [
height * 0.5
])
ctx.fill()
if (!disabled) ctx.stroke()
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR
ctx.font = `${LiteGraph.NODE_TEXT_SIZE}px ${LiteGraph.NODE_FONT}`
ctx.textAlign = 'center'
ctx.fillText(label, width * 0.5, y + height * 0.7)
ctx.restore()
}
/** Horizontal centre of a row header's remove (✕) hit target. */
const removeButtonCenterX = (width: number) =>
width - CANVAS_MARGIN - LiteGraph.NODE_WIDGET_HEIGHT * 0.5
/** Draw a row header (label on the left, ✕ on the right) on the canvas. */
function drawGroupRowHeader(
ctx: CanvasRenderingContext2D,
width: number,
y: number,
label: string,
removable: boolean
): void {
const height = LiteGraph.NODE_WIDGET_HEIGHT
ctx.save()
ctx.font = `${LiteGraph.NODE_TEXT_SIZE}px ${LiteGraph.NODE_FONT}`
ctx.fillStyle = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR
ctx.textAlign = 'left'
ctx.fillText(label, CANVAS_MARGIN, y + height * 0.7)
if (removable) {
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR
ctx.textAlign = 'center'
ctx.fillText('\u2715', removeButtonCenterX(width), y + height * 0.7)
}
ctx.restore()
}
const countGroupRows = (group: string, node: LGraphNode): number =>
(node.widgets ?? []).reduce(
(count, w) =>
headerRowIndex(group, w.name) !== undefined ? count + 1 : count,
0
)
/** Build a row's header + field widgets, returning them detached from the node. */
function createRow(
group: string,
row: number,
state: DynamicGroupState,
node: DynamicGroupNode
): IBaseWidget[] {
const { addNodeInput } = useLitegraphService()
const startLen = node.widgets!.length
const header = node.addCustomWidget({
name: rowHeaderName(group, row),
type: 'dynamic_group_row',
value: row,
y: 0,
serialize: false,
callback: undefined as IBaseWidget['callback'],
draw(
this: IBaseWidget,
ctx: CanvasRenderingContext2D,
_node: LGraphNode,
width: number,
y: number
) {
const idx = headerRowIndex(group, this.name) ?? 0
const label = t('dynamicGroup.row', { index: idx + 1 })
drawGroupRowHeader(ctx, width, y, label, !!this.options?.removable)
},
mouse(this: IBaseWidget, event: CanvasPointerEvent, pos: Point) {
if (event.type !== 'pointerup' || !this.options?.removable) return false
const half = LiteGraph.NODE_WIDGET_HEIGHT * 0.5
if (Math.abs(pos[0] - removeButtonCenterX(node.size[0])) > half)
return false
const idx = headerRowIndex(group, this.name)
if (idx !== undefined) removeRow(group, idx, node)
return true
},
options: { serialize: false, socketless: true, removable: row >= state.min }
})
header.callback = function (this: IBaseWidget) {
const idx = headerRowIndex(group, this.name)
if (idx !== undefined) removeRow(group, idx, node)
}
for (const spec of state.inputSpecs)
addNodeInput(node, {
...spec,
name: fieldName(group, row, spec.name),
display_name: spec.display_name ?? spec.name
})
return node.widgets!.splice(startLen)
}
function insertRowAfterGroup(
group: string,
node: LGraphNode,
rowWidgets: IBaseWidget[]
): void {
const lastIdx = node.widgets!.findLastIndex(
(w) => w.name === group || w.name.startsWith(`${group}.`)
)
node.widgets!.splice(lastIdx + 1, 0, ...rowWidgets)
}
function syncController(group: string, node: DynamicGroupNode): void {
const state = node.comfyDynamic.dynamicGroup[group]
const controller = node.widgets?.find((w) => w.name === group)
if (!state || !controller) return
controller.options ??= {}
controller.options.disabled = countGroupRows(group, node) >= state.max
node.size[1] = node.computeSize([...node.size])[1]
}
function addRow(group: string, node: DynamicGroupNode): void {
const state = node.comfyDynamic.dynamicGroup[group]
if (!state) return
node.widgets ??= []
const row = countGroupRows(group, node)
if (row >= state.max) return
insertRowAfterGroup(group, node, createRow(group, row, state, node))
syncController(group, node)
app.canvas?.setDirty(true, true)
}
function removeRow(group: string, row: number, node: DynamicGroupNode): void {
const state = node.comfyDynamic.dynamicGroup[group]
if (!state || row < state.min) return
for (const w of remove(node.widgets!, (w) =>
belongsToRow(group, w.name, row)
))
w.onRemove?.()
remove(node.inputs, (inp) => belongsToRow(group, inp.name, row))
for (const w of node.widgets ?? []) {
const headerRow = headerRowIndex(group, w.name)
if (headerRow !== undefined && headerRow > row) {
w.name = rowHeaderName(group, headerRow - 1)
w.options ??= {}
w.options.removable = headerRow - 1 >= state.min
continue
}
const shifted = shiftedFieldName(group, w.name, row)
if (shifted !== undefined) w.name = shifted
}
for (const inp of node.inputs) {
const shifted = shiftedFieldName(group, inp.name, row)
if (shifted === undefined) continue
inp.name = shifted
if (inp.widget) inp.widget.name = shifted
}
syncController(group, node)
app.canvas?.setDirty(true, true)
}
/** Rebuild the group from scratch to hold exactly `count` rows. */
function rebuildRows(group: string, count: number, node: DynamicGroupNode) {
const state = node.comfyDynamic.dynamicGroup[group]
if (!state) return
node.widgets ??= []
const isRowMember = (name: string) => name.startsWith(`${group}.`)
for (const w of remove(node.widgets, (w) => isRowMember(w.name)))
w.onRemove?.()
remove(node.inputs, (inp) => isRowMember(inp.name))
const insertAt = node.widgets.findIndex((w) => w.name === group) + 1
const rowWidgets: IBaseWidget[] = []
for (let row = 0; row < count; row++)
rowWidgets.push(...createRow(group, row, state, node))
node.widgets.splice(insertAt, 0, ...rowWidgets)
}
function dynamicGroupWidget(
node: LGraphNode,
inputName: string,
untypedInputData: InputSpec,
_appArg: ComfyApp
) {
const parseResult = zDynamicGroupInputSpec.safeParse(untypedInputData)
if (!parseResult.success) throw new Error('invalid DynamicGroup spec')
const [, { template, min, max }] = parseResult.data
const toSpecs = (
inputs: Record<string, InputSpec> | undefined,
isOptional: boolean
) =>
Object.entries(inputs ?? {}).map(([name, spec]) =>
transformInputSpecV1ToV2(spec, { name, isOptional })
)
const inputSpecs = [
...toSpecs(template.required, false),
...toSpecs(template.optional, true)
]
withComfyDynamicGroup(node)
const typedNode = node as DynamicGroupNode
typedNode.comfyDynamic.dynamicGroup[inputName] = { min, max, inputSpecs }
node.widgets ??= []
const controller = node.addCustomWidget({
name: inputName,
type: 'dynamic_group_add',
value: min,
y: 0,
serialize: true,
callback: () => addRow(inputName, typedNode),
draw(
this: IBaseWidget,
ctx: CanvasRenderingContext2D,
_node: LGraphNode,
width: number,
y: number
) {
drawGroupButton(
ctx,
width,
y,
t('dynamicGroup.addRow'),
!!this.options?.disabled
)
},
mouse(this: IBaseWidget, event: CanvasPointerEvent) {
if (event.type !== 'pointerup' || this.options?.disabled) return false
addRow(inputName, typedNode)
return true
},
options: { serialize: false, socketless: true, disabled: false }
})
Object.defineProperty(controller, 'value', {
get() {
return countGroupRows(inputName, typedNode)
},
set(count: unknown) {
if (typeof count !== 'number') return
rebuildRows(inputName, count, typedNode)
syncController(inputName, typedNode)
},
configurable: true
})
controller.value = min
return { widget: controller }
}
export const dynamicWidgets = {
COMFY_DYNAMICCOMBO_V3: dynamicComboWidget,
COMFY_DYNAMICGROUP_V3: dynamicGroupWidget
}
const dynamicInputs: Record<
string,
(node: LGraphNode, inputSpec: InputSpecV2) => void

View File

@@ -71,6 +71,7 @@ export interface IWidgetOptions<TValues = unknown> {
// Vue widget options
disabled?: boolean
removable?: boolean
useGrouping?: boolean
placeholder?: string
showThumbnails?: boolean

View File

@@ -325,6 +325,11 @@
"signUpWithGithub": "إنشاء حساب باستخدام Github",
"signUpWithGoogle": "إنشاء حساب باستخدام Google",
"title": "إنشاء حساب"
},
"turnstile": {
"expired": "انتهت صلاحية التحقق. يرجى إكمال التحقق مرة أخرى.",
"failed": "فشل التحقق. يرجى المحاولة مرة أخرى.",
"submitBlockedHint": "يرجى إكمال التحقق أعلاه لتفعيل التسجيل."
}
},
"batch": {
@@ -3742,12 +3747,14 @@
"keepSubscription": "الاحتفاظ بالاشتراك",
"title": "إلغاء الاشتراك"
},
"cancelPlan": "إلغاء الخطة",
"cancelSuccess": "تم إلغاء الاشتراك بنجاح",
"canceled": "تم الإلغاء",
"canceledCard": {
"description": "لن يتم خصم أي رسوم أخرى منك. ستبقى ميزاتك نشطة حتى {date}.",
"title": "تم إلغاء اشتراكك"
},
"changePlan": "تغيير الخطة",
"changeTo": "تغيير إلى {plan}",
"comfyCloud": "Comfy Cloud",
"comfyCloudLogo": "شعار Comfy Cloud",
@@ -3776,6 +3783,7 @@
"paymentPageBlocked": "تعذر فتح صفحة الدفع — يرجى المحاولة مرة أخرى",
"title": "تغيير إلى خطة {plan}؟"
},
"endsOnDate": "ينتهي في {date}",
"enterprise": {
"cta": "اعرف المزيد",
"flexibility": "تبحث عن مزيد من المرونة أو ميزات مخصصة؟",
@@ -3785,6 +3793,9 @@
},
"everythingInPlus": "كل ما في {plan}، بالإضافة إلى:",
"expiresDate": "ينتهي في {date}",
"freePerks": {
"maxRuntime": "مدة تشغيل قصوى {duration}"
},
"freeTier": {
"description": "تشمل خطتك المجانية {credits} رصيد شهري لتجربة Comfy Cloud.",
"descriptionGeneric": "تشمل خطتك المجانية رصيدًا شهريًا لتجربة Comfy Cloud.",
@@ -3812,7 +3823,7 @@
"inviteUpTo": "ادعُ حتى",
"invoiceHistory": "سجل الفواتير",
"learnMore": "معرفة المزيد",
"managePayment": "إدارة الدفع",
"manageBilling": "إدارة الفواتير",
"managePlan": "إدارة الخطة",
"manageSubscription": "إدارة الاشتراك",
"maxDuration": {
@@ -3826,7 +3837,6 @@
"maxMembersLabel": "الحد الأقصى للأعضاء",
"member": "عضو",
"memberCount": "{count} عضو | {count} أعضاء",
"membersLabel": "حتى {count} عضو",
"messageSupport": "مراسلة الدعم",
"monthly": "شهري",
"monthlyBonusDescription": "مكافأة الرصيد الشهرية",
@@ -3842,17 +3852,19 @@
"mostPopular": "الأكثر شيوعًا",
"needTeamWorkspace": "هل تحتاج إلى مساحة عمل للفريق؟",
"nextBillingCycle": "دورة الفوترة التالية",
"nextMonthInvoice": "فاتورة الشهر القادم",
"outOfCreditsDescription": "أضف المزيد من الرصيد للمتابعة في التوليد.",
"outOfCreditsTitle": "نفد رصيدك. سيتم إعادة التعبئة {date}",
"outOfCreditsTitleNoDate": "نفد رصيدك",
"partnerNodesBalance": "رصيد \"عُقَد الشريك\"",
"partnerNodesCredits": "رصيد العقد الشريكة",
"partnerNodesDescription": "لتشغيل النماذج التجارية/المملوكة",
"partnerNodesPricingTable": "جدول أسعار Partner Nodes",
"perMonth": "دولار أمريكي / شهر",
"personalHeader": "الخطط الشخصية للاستخدام الفردي فقط. {action}",
"personalHeaderAction": "لإضافة زملاء، اشترك في خطة الفريق.",
"personalWorkspace": "مساحة العمل الشخصية",
"planLoadError": "تعذر تحميل تفاصيل خطتك.",
"planLoadErrorRetry": "حاول مرة أخرى",
"planScope": {
"personal": "للاستخدام الشخصي",
"team": "للفرق"
@@ -3911,11 +3923,13 @@
"pricingBlurbEnterprise": "مناقشات المؤسسات",
"pricingBlurbQuestions": "الاستفسارات",
"pricingBlurbSeeDetails": "اعرض التفاصيل",
"reactivatePlan": "إعادة تفعيل الخطة",
"refillsDate": "إعادة التعبئة {date}",
"refillsNextCycle": "إعادة التعبئة في الدورة التالية",
"refreshCredits": "تحديث الرصيد",
"remaining": "متبقي",
"renewsDate": "تجديد في {date}",
"renewsOnDate": "يتجدد في {date}",
"required": {
"pollingFailed": "فشل تفعيل الاشتراك",
"pollingSuccess": "تم تفعيل الاشتراك بنجاح!",
@@ -3930,6 +3944,7 @@
"saveYearly": "وفّر 20%",
"saveYearlyUpTo": "وفر حتى ٢٠٪",
"soloUseOnly": "للاستخدام الفردي فقط",
"subscribe": "اشترك",
"subscribeFailed": "فشل الاشتراك",
"subscribeForMore": "ترقية",
"subscribeNow": "اشترك الآن",
@@ -3949,6 +3964,12 @@
},
"teamHeader": "للفرق التي ترغب في التعاون. تحتاج إلى المزيد من الأعضاء؟ {learnMore} حول المؤسسات.",
"teamHeaderLearnMore": "اعرف المزيد",
"teamPerks": {
"concurrentRuns": "يمكن للأعضاء تشغيل سير العمل في نفس الوقت",
"inviteMembers": "دعوة الأعضاء",
"rolePermissions": "أذونات حسب الدور",
"sharedCreditPool": "رصيد مشترك لجميع الأعضاء"
},
"teamPlan": {
"changePlan": "تغيير الخطة",
"comingSoonLabel": "قريبًا:",
@@ -3965,6 +3986,8 @@
"tagline": "اختر اشتراك الرصيد الشهري الخاص بك. احصل على خصم أكبر مع اشتراك رصيد أكبر.",
"unavailable": "خطة الفريق هذه غير متوفرة حالياً."
},
"teamPlanIncludes": "تشمل خطتك كل ما في {plan}، بالإضافة إلى:",
"teamPlanName": "فريق",
"teamWorkspace": "مساحة عمل الفريق",
"tierNameYearly": "{name} سنوي",
"tiers": {

View File

@@ -2237,6 +2237,11 @@
"slots": "Node Slots Error",
"widgets": "Node Widgets Error"
},
"dynamicGroup": {
"addRow": "Add row",
"removeRow": "Remove row",
"row": "Row {index}"
},
"oauth": {
"consent": {
"allow": "Continue",
@@ -2678,7 +2683,6 @@
"teamHeaderLearnMore": "Learn more",
"personalHeader": "Personal plans are for individual use only. {action}",
"personalHeaderAction": "To add teammates, subscribe to the team plan.",
"whatsIncluded": "What's included:",
"everythingInPlus": "Everything in {plan}, plus:",
"monthlyCredits": "monthly credits",
"videoEstimate": "Generates ~{count} 5s videos*",

View File

@@ -325,6 +325,11 @@
"signUpWithGithub": "Registrarse con Github",
"signUpWithGoogle": "Registrarse con Google",
"title": "Crea una cuenta"
},
"turnstile": {
"expired": "La verificación ha expirado. Por favor, completa el desafío nuevamente.",
"failed": "La verificación falló. Por favor, inténtalo de nuevo.",
"submitBlockedHint": "Completa el desafío de verificación arriba para habilitar el registro."
}
},
"batch": {
@@ -3742,12 +3747,14 @@
"keepSubscription": "Mantener suscripción",
"title": "Cancelar suscripción"
},
"cancelPlan": "Cancelar plan",
"cancelSuccess": "Suscripción cancelada correctamente",
"canceled": "Cancelada",
"canceledCard": {
"description": "No se te cobrará de nuevo. Tus funciones seguirán activas hasta {date}.",
"title": "Tu suscripción ha sido cancelada"
},
"changePlan": "Cambiar plan",
"changeTo": "Cambiar a {plan}",
"comfyCloud": "Comfy Cloud",
"comfyCloudLogo": "Logo de Comfy Cloud",
@@ -3776,6 +3783,7 @@
"paymentPageBlocked": "No se pudo abrir la página de pago — por favor, inténtalo de nuevo",
"title": "¿Cambiar al plan {plan}?"
},
"endsOnDate": "Finaliza el {date}",
"enterprise": {
"cta": "Saber más",
"flexibility": "¿Buscas más flexibilidad o funciones personalizadas?",
@@ -3785,6 +3793,9 @@
},
"everythingInPlus": "Todo en {plan}, además:",
"expiresDate": "Caduca el {date}",
"freePerks": {
"maxRuntime": "{duration} de tiempo máximo de ejecución"
},
"freeTier": {
"description": "Tu plan gratuito incluye {credits} créditos cada mes para probar Comfy Cloud.",
"descriptionGeneric": "Tu plan gratuito incluye una asignación mensual de créditos para probar Comfy Cloud.",
@@ -3812,7 +3823,7 @@
"inviteUpTo": "Invita hasta",
"invoiceHistory": "Historial de facturas",
"learnMore": "Más información",
"managePayment": "Gestionar pago",
"manageBilling": "Gestionar facturación",
"managePlan": "Gestionar plan",
"manageSubscription": "Gestionar suscripción",
"maxDuration": {
@@ -3826,7 +3837,6 @@
"maxMembersLabel": "Máx. miembros",
"member": "miembro",
"memberCount": "{count} miembro | {count} miembros",
"membersLabel": "Hasta {count} miembros",
"messageSupport": "Contactar con soporte",
"monthly": "Mensual",
"monthlyBonusDescription": "Bono de créditos mensual",
@@ -3842,17 +3852,19 @@
"mostPopular": "Más popular",
"needTeamWorkspace": "¿Necesitas un espacio de trabajo en equipo?",
"nextBillingCycle": "próximo ciclo de facturación",
"nextMonthInvoice": "Factura del próximo mes",
"outOfCreditsDescription": "Agrega más créditos para continuar generando.",
"outOfCreditsTitle": "Te has quedado sin créditos. Recarga de créditos {date}",
"outOfCreditsTitleNoDate": "Te has quedado sin créditos",
"partnerNodesBalance": "Saldo de créditos de \"Nodos de Partners\"",
"partnerNodesCredits": "Créditos de Nodos de Socio",
"partnerNodesDescription": "Para ejecutar modelos comerciales/propietarios",
"partnerNodesPricingTable": "Tabla de precios de Partner Nodes",
"perMonth": "USD / mes",
"personalHeader": "Los planes personales son solo para uso individual. {action}",
"personalHeaderAction": "Para agregar compañeros, suscríbete al plan de equipo.",
"personalWorkspace": "Espacio de trabajo personal",
"planLoadError": "No pudimos cargar los detalles de tu plan.",
"planLoadErrorRetry": "Intentar de nuevo",
"planScope": {
"personal": "Para uso personal",
"team": "Para equipos"
@@ -3911,11 +3923,13 @@
"pricingBlurbEnterprise": "discusiones enterprise",
"pricingBlurbQuestions": "preguntas",
"pricingBlurbSeeDetails": "ver detalles",
"reactivatePlan": "Reactivar plan",
"refillsDate": "Recargas {date}",
"refillsNextCycle": "Recargas en el próximo ciclo",
"refreshCredits": "Actualizar créditos",
"remaining": "restante",
"renewsDate": "Se renueva el {date}",
"renewsOnDate": "Renueva el {date}",
"required": {
"pollingFailed": "Error al activar la suscripción",
"pollingSuccess": "¡Suscripción activada correctamente!",
@@ -3930,6 +3944,7 @@
"saveYearly": "Ahorra 20%",
"saveYearlyUpTo": "Ahorra hasta un 20%",
"soloUseOnly": "Solo para uso individual",
"subscribe": "Suscribirse",
"subscribeFailed": "No se pudo suscribir",
"subscribeForMore": "Mejorar",
"subscribeNow": "Suscribirse Ahora",
@@ -3949,6 +3964,12 @@
},
"teamHeader": "Para equipos que desean colaborar. ¿Necesitas más miembros? {learnMore} sobre enterprise.",
"teamHeaderLearnMore": "Saber más",
"teamPerks": {
"concurrentRuns": "Los miembros pueden ejecutar flujos de trabajo simultáneamente",
"inviteMembers": "Invitar miembros",
"rolePermissions": "Permisos basados en roles",
"sharedCreditPool": "Bolsa de créditos compartida para todos los miembros"
},
"teamPlan": {
"changePlan": "Cambiar plan",
"comingSoonLabel": "Próximamente:",
@@ -3965,6 +3986,8 @@
"tagline": "Elige tu propia suscripción mensual de créditos. Obtén un mayor descuento con una suscripción de más créditos.",
"unavailable": "Este plan de equipo no está disponible en este momento."
},
"teamPlanIncludes": "Tu plan incluye todo en {plan}, además de:",
"teamPlanName": "Equipo",
"teamWorkspace": "Espacio de trabajo en equipo",
"tierNameYearly": "{name} Anual",
"tiers": {

View File

@@ -325,6 +325,11 @@
"signUpWithGithub": "ثبت‌نام با Github",
"signUpWithGoogle": "ثبت‌نام با Google",
"title": "ایجاد حساب کاربری"
},
"turnstile": {
"expired": "اعتبار تأییدیه به پایان رسیده است. لطفاً دوباره چالش را تکمیل کنید.",
"failed": "تأییدیه ناموفق بود. لطفاً دوباره تلاش کنید.",
"submitBlockedHint": "برای فعال‌سازی ثبت‌نام، ابتدا چالش تأییدیه بالا را تکمیل کنید."
}
},
"batch": {
@@ -3754,12 +3759,14 @@
"keepSubscription": "حفظ اشتراک",
"title": "لغو اشتراک"
},
"cancelPlan": "لغو پلن",
"cancelSuccess": "اشتراک با موفقیت لغو شد",
"canceled": "لغو شد",
"canceledCard": {
"description": "دیگر هزینه‌ای از شما دریافت نمی‌شود. امکانات شما تا تاریخ {date} فعال خواهد بود.",
"title": "اشتراک شما لغو شده است"
},
"changePlan": "تغییر پلن",
"changeTo": "تغییر به {plan}",
"comfyCloud": "Comfy Cloud",
"comfyCloudLogo": "لوگوی Comfy Cloud",
@@ -3788,6 +3795,7 @@
"paymentPageBlocked": "امکان باز کردن صفحه پرداخت وجود ندارد — لطفاً دوباره تلاش کنید",
"title": "تغییر به پلن {plan}؟"
},
"endsOnDate": "پایان در تاریخ {date}",
"enterprise": {
"cta": "بیشتر بدانید",
"flexibility": "به دنبال امکانات یا انعطاف‌پذیری بیشتر هستید؟",
@@ -3797,6 +3805,9 @@
},
"everythingInPlus": "همه امکانات {plan}، به‌علاوه:",
"expiresDate": "انقضا در {date}",
"freePerks": {
"maxRuntime": "حداکثر زمان اجرا {duration}"
},
"freeTier": {
"description": "طرح رایگان شما شامل {credits} اعتبار در هر ماه برای استفاده از Comfy Cloud است.",
"descriptionGeneric": "طرح رایگان شما شامل اعتبار ماهانه برای استفاده از Comfy Cloud است.",
@@ -3824,7 +3835,7 @@
"inviteUpTo": "دعوت تا سقف",
"invoiceHistory": "تاریخچه فاکتورها",
"learnMore": "اطلاعات بیشتر",
"managePayment": "مدیریت پرداخت",
"manageBilling": "مدیریت صورتحساب",
"managePlan": "مدیریت طرح",
"manageSubscription": "مدیریت اشتراک",
"maxDuration": {
@@ -3838,7 +3849,6 @@
"maxMembersLabel": "حداکثر اعضا",
"member": "عضو",
"memberCount": "{count} عضو",
"membersLabel": "تا {count} عضو",
"messageSupport": "پیام به پشتیبانی",
"monthly": "ماهانه",
"monthlyBonusDescription": "پاداش ماهانه اعتبار",
@@ -3854,17 +3864,19 @@
"mostPopular": "محبوب‌ترین",
"needTeamWorkspace": "به فضای کاری تیمی نیاز دارید؟",
"nextBillingCycle": "چرخه صورتحساب بعدی",
"nextMonthInvoice": "صورتحساب ماه آینده",
"outOfCreditsDescription": "برای ادامه تولید، اعتبار بیشتری اضافه کنید.",
"outOfCreditsTitle": "اعتبار شما تمام شده است. شارژ مجدد در {date}",
"outOfCreditsTitleNoDate": "اعتبار شما تمام شده است",
"partnerNodesBalance": "اعتبار «Partner Nodes»",
"partnerNodesCredits": "قیمت‌گذاری Partner Nodes",
"partnerNodesDescription": "برای اجرای مدل‌های تجاری/اختصاصی",
"partnerNodesPricingTable": "جدول قیمت‌گذاری Partner Nodeها",
"perMonth": "/ ماه",
"personalHeader": "پلن‌های شخصی فقط برای استفاده فردی هستند. {action}",
"personalHeaderAction": "برای افزودن هم‌تیمی، به پلن تیمی ارتقا دهید.",
"personalWorkspace": "فضای کاری شخصی",
"planLoadError": "امکان بارگذاری جزئیات پلن شما وجود ندارد.",
"planLoadErrorRetry": "تلاش مجدد",
"planScope": {
"personal": "برای استفاده شخصی",
"team": "برای تیم‌ها"
@@ -3923,11 +3935,13 @@
"pricingBlurbEnterprise": "بحث‌های سازمانی",
"pricingBlurbQuestions": "سؤالات",
"pricingBlurbSeeDetails": "جزئیات را ببینید",
"reactivatePlan": "فعال‌سازی مجدد پلن",
"refillsDate": "شارژ مجدد در {date}",
"refillsNextCycle": "شارژ مجدد در چرخه بعدی",
"refreshCredits": "به‌روزرسانی اعتبارها",
"remaining": "باقی‌مانده",
"renewsDate": "تمدید در {date}",
"renewsOnDate": "تمدید در تاریخ {date}",
"required": {
"pollingFailed": "فعال‌سازی اشتراک ناموفق بود",
"pollingSuccess": "اشتراک با موفقیت فعال شد!",
@@ -3942,6 +3956,7 @@
"saveYearly": "٪۲۰ صرفه‌جویی",
"saveYearlyUpTo": "تا ۲۰٪ صرفه‌جویی کنید",
"soloUseOnly": "فقط برای استفاده فردی",
"subscribe": "اشتراک",
"subscribeFailed": "اشتراک‌گذاری ناموفق بود",
"subscribeForMore": "ارتقاء",
"subscribeNow": "هم‌اکنون اشتراک بگیرید",
@@ -3961,6 +3976,12 @@
},
"teamHeader": "برای تیم‌هایی که قصد همکاری دارند. به اعضای بیشتری نیاز دارید؟ {learnMore} درباره پلن سازمانی.",
"teamHeaderLearnMore": "بیشتر بدانید",
"teamPerks": {
"concurrentRuns": "اعضا می‌توانند workflowها را به صورت همزمان اجرا کنند",
"inviteMembers": "دعوت اعضا",
"rolePermissions": "دسترسی مبتنی بر نقش",
"sharedCreditPool": "استخر اعتبار مشترک برای همه اعضا"
},
"teamPlan": {
"changePlan": "تغییر پلن",
"comingSoonLabel": "به‌زودی:",
@@ -3977,6 +3998,8 @@
"tagline": "اشتراک اعتباری ماهانه دلخواه خود را انتخاب کنید. با اعتبار بیشتر، تخفیف بیشتری دریافت کنید.",
"unavailable": "این طرح تیمی در حال حاضر در دسترس نیست."
},
"teamPlanIncludes": "پلن شما شامل همه امکانات {plan} و همچنین:",
"teamPlanName": "تیمی",
"teamWorkspace": "فضای کاری تیمی",
"tierNameYearly": "{name} سالانه",
"tiers": {

View File

@@ -325,6 +325,11 @@
"signUpWithGithub": "S'inscrire avec Github",
"signUpWithGoogle": "S'inscrire avec Google",
"title": "Créer un compte"
},
"turnstile": {
"expired": "La vérification a expiré. Veuillez recommencer le défi.",
"failed": "Échec de la vérification. Veuillez réessayer.",
"submitBlockedHint": "Veuillez compléter le défi de vérification ci-dessus pour activer l'inscription."
}
},
"batch": {
@@ -3742,12 +3747,14 @@
"keepSubscription": "Conserver l'abonnement",
"title": "Annuler l'abonnement"
},
"cancelPlan": "Annuler labonnement",
"cancelSuccess": "Abonnement annulé avec succès",
"canceled": "Annulé",
"canceledCard": {
"description": "Vous ne serez plus facturé. Vos fonctionnalités restent actives jusqu'au {date}.",
"title": "Votre abonnement a été annulé"
},
"changePlan": "Changer dabonnement",
"changeTo": "Changer pour {plan}",
"comfyCloud": "Comfy Cloud",
"comfyCloudLogo": "Logo Comfy Cloud",
@@ -3776,6 +3783,7 @@
"paymentPageBlocked": "Impossible douvrir la page de paiement — veuillez réessayer",
"title": "Passer au plan {plan} ?"
},
"endsOnDate": "Se termine le {date}",
"enterprise": {
"cta": "En savoir plus",
"flexibility": "Vous cherchez plus de flexibilité ou des fonctionnalités personnalisées ?",
@@ -3785,6 +3793,9 @@
},
"everythingInPlus": "Tout ce qui est dans {plan}, plus :",
"expiresDate": "Expire le {date}",
"freePerks": {
"maxRuntime": "{duration} de temps dexécution maximum"
},
"freeTier": {
"description": "Votre plan gratuit inclut {credits} crédits chaque mois pour essayer Comfy Cloud.",
"descriptionGeneric": "Votre plan gratuit inclut une allocation mensuelle de crédits pour essayer Comfy Cloud.",
@@ -3812,7 +3823,7 @@
"inviteUpTo": "Invitez jusquà",
"invoiceHistory": "Historique des factures",
"learnMore": "En savoir plus",
"managePayment": "Gérer le paiement",
"manageBilling": "Gérer la facturation",
"managePlan": "Gérer le forfait",
"manageSubscription": "Gérer l'abonnement",
"maxDuration": {
@@ -3826,7 +3837,6 @@
"maxMembersLabel": "Nombre max. de membres",
"member": "membre",
"memberCount": "{count} membre | {count} membres",
"membersLabel": "Jusqu'à {count} membres",
"messageSupport": "Contacter le support",
"monthly": "Mensuel",
"monthlyBonusDescription": "Bonus de crédits mensuel",
@@ -3842,17 +3852,19 @@
"mostPopular": "Le plus populaire",
"needTeamWorkspace": "Besoin dun espace de travail déquipe ?",
"nextBillingCycle": "prochain cycle de facturation",
"nextMonthInvoice": "Facture du mois prochain",
"outOfCreditsDescription": "Ajoutez des crédits pour continuer à générer.",
"outOfCreditsTitle": "Vous n'avez plus de crédits. Recharge le {date}",
"outOfCreditsTitleNoDate": "Vous n'avez plus de crédits",
"partnerNodesBalance": "Solde de crédits \"Nœuds Partenaires\"",
"partnerNodesCredits": "Crédits Nœuds Partenaires",
"partnerNodesDescription": "Pour exécuter des modèles commerciaux/propriétaires",
"partnerNodesPricingTable": "Tableau des tarifs des Partner Nodes",
"perMonth": "USD / mois",
"personalHeader": "Les plans personnels sont réservés à un usage individuel. {action}",
"personalHeaderAction": "Pour ajouter des coéquipiers, abonnez-vous au plan équipe.",
"personalWorkspace": "Espace de travail personnel",
"planLoadError": "Nous n'avons pas pu charger les détails de votre abonnement.",
"planLoadErrorRetry": "Réessayer",
"planScope": {
"personal": "Pour usage personnel",
"team": "Pour les équipes"
@@ -3911,11 +3923,13 @@
"pricingBlurbEnterprise": "discussions entreprise",
"pricingBlurbQuestions": "questions",
"pricingBlurbSeeDetails": "voir les détails",
"reactivatePlan": "Réactiver labonnement",
"refillsDate": "Recharge le {date}",
"refillsNextCycle": "Recharge au prochain cycle",
"refreshCredits": "Actualiser les crédits",
"remaining": "restant",
"renewsDate": "Renouvellement le {date}",
"renewsOnDate": "Renouvellement le {date}",
"required": {
"pollingFailed": "Échec de l'activation de l'abonnement",
"pollingSuccess": "Abonnement activé avec succès !",
@@ -3930,6 +3944,7 @@
"saveYearly": "Économisez 20 %",
"saveYearlyUpTo": "Économisez jusquà 20 %",
"soloUseOnly": "Usage solo uniquement",
"subscribe": "Sabonner",
"subscribeFailed": "Échec de l'abonnement",
"subscribeForMore": "Mettre à niveau",
"subscribeNow": "S'abonner maintenant",
@@ -3949,6 +3964,12 @@
},
"teamHeader": "Pour les équipes souhaitant collaborer. Besoin de plus de membres ? {learnMore} sur loffre entreprise.",
"teamHeaderLearnMore": "En savoir plus",
"teamPerks": {
"concurrentRuns": "Les membres peuvent exécuter des workflows simultanément",
"inviteMembers": "Inviter des membres",
"rolePermissions": "Permissions basées sur les rôles",
"sharedCreditPool": "Crédit partagé pour tous les membres"
},
"teamPlan": {
"changePlan": "Changer de plan",
"comingSoonLabel": "Bientôt disponible :",
@@ -3965,6 +3986,8 @@
"tagline": "Choisissez votre propre abonnement mensuel de crédits. Bénéficiez dune plus grande remise avec un abonnement de crédits plus important.",
"unavailable": "Ce forfait d'équipe n'est pas disponible pour le moment."
},
"teamPlanIncludes": "Votre abonnement inclut tout dans {plan}, plus :",
"teamPlanName": "Équipe",
"teamWorkspace": "Espace de travail déquipe",
"tierNameYearly": "{name} Annuel",
"tiers": {

View File

@@ -325,6 +325,11 @@
"signUpWithGithub": "Githubでサインアップ",
"signUpWithGoogle": "Googleでサインアップ",
"title": "アカウントを作成する"
},
"turnstile": {
"expired": "認証の有効期限が切れました。再度チャレンジを完了してください。",
"failed": "認証に失敗しました。もう一度お試しください。",
"submitBlockedHint": "上記の認証チャレンジを完了すると、サインアップが有効になります。"
}
},
"batch": {
@@ -3742,12 +3747,14 @@
"keepSubscription": "サブスクリプションを維持する",
"title": "サブスクリプションのキャンセル"
},
"cancelPlan": "プランをキャンセル",
"cancelSuccess": "サブスクリプションが正常にキャンセルされました",
"canceled": "キャンセル済み",
"canceledCard": {
"description": "今後請求されることはありません。{date}まで機能は有効です。",
"title": "サブスクリプションはキャンセルされました"
},
"changePlan": "プランを変更",
"changeTo": "{plan}に変更",
"comfyCloud": "Comfy Cloud",
"comfyCloudLogo": "Comfy Cloud ロゴ",
@@ -3776,6 +3783,7 @@
"paymentPageBlocked": "お支払いページを開けませんでした。再度お試しください。",
"title": "{plan}プランに変更しますか?"
},
"endsOnDate": "{date}に終了します",
"enterprise": {
"cta": "詳細を見る",
"flexibility": "より柔軟な対応やカスタム機能をご希望ですか?",
@@ -3785,6 +3793,9 @@
},
"everythingInPlus": "{plan}のすべて、さらに:",
"expiresDate": "{date} に期限切れ",
"freePerks": {
"maxRuntime": "最大実行時間:{duration}"
},
"freeTier": {
"description": "無料プランには、Comfy Cloudをお試しいただける毎月{credits}クレジットが含まれています。",
"descriptionGeneric": "無料プランには、Comfy Cloudをお試しいただける毎月のクレジット枠が含まれています。",
@@ -3812,7 +3823,7 @@
"inviteUpTo": "最大 {count} 人を招待",
"invoiceHistory": "請求履歴",
"learnMore": "詳細を見る",
"managePayment": "支払いを管理",
"manageBilling": "請求管理",
"managePlan": "プランを管理",
"manageSubscription": "サブスクリプションを管理",
"maxDuration": {
@@ -3826,7 +3837,6 @@
"maxMembersLabel": "最大メンバー数",
"member": "メンバー",
"memberCount": "{count}名のメンバー",
"membersLabel": "{count}名までのメンバー",
"messageSupport": "サポートに連絡",
"monthly": "月額",
"monthlyBonusDescription": "月間クレジットボーナス",
@@ -3842,17 +3852,19 @@
"mostPopular": "最も人気",
"needTeamWorkspace": "チームワークスペースが必要ですか?",
"nextBillingCycle": "次の請求サイクル",
"nextMonthInvoice": "翌月の請求書",
"outOfCreditsDescription": "生成を続けるにはクレジットを追加してください。",
"outOfCreditsTitle": "クレジットがありません。{date}に補充されます",
"outOfCreditsTitleNoDate": "クレジットがありません",
"partnerNodesBalance": "\"パートナーノード\" クレジット残高",
"partnerNodesCredits": "パートナーノードクレジット",
"partnerNodesDescription": "商用/独自モデルの実行用",
"partnerNodesPricingTable": "パートナーノードの料金表",
"perMonth": "USD / 月",
"personalHeader": "個人プランは個人利用専用です。{action}",
"personalHeaderAction": "チームメンバーを追加するには、チームプランにご加入ください。",
"personalWorkspace": "個人ワークスペース",
"planLoadError": "プランの詳細を読み込めませんでした。",
"planLoadErrorRetry": "再試行",
"planScope": {
"personal": "個人向け",
"team": "チーム向け"
@@ -3911,11 +3923,13 @@
"pricingBlurbEnterprise": "エンタープライズのご相談",
"pricingBlurbQuestions": "ご質問",
"pricingBlurbSeeDetails": "詳細を見る",
"reactivatePlan": "プランを再開",
"refillsDate": "{date}に補充",
"refillsNextCycle": "次のサイクルで補充",
"refreshCredits": "クレジットを更新",
"remaining": "残り",
"renewsDate": "{date} に更新",
"renewsOnDate": "{date}に更新されます",
"required": {
"pollingFailed": "サブスクリプションの有効化に失敗しました",
"pollingSuccess": "サブスクリプションが有効化されました!",
@@ -3930,6 +3944,7 @@
"saveYearly": "20%お得",
"saveYearlyUpTo": "最大20%お得",
"soloUseOnly": "個人利用のみ",
"subscribe": "購読する",
"subscribeFailed": "購読に失敗しました",
"subscribeForMore": "アップグレード",
"subscribeNow": "今すぐ購読",
@@ -3949,6 +3964,12 @@
},
"teamHeader": "コラボレーションを希望するチーム向け。さらに多くのメンバーが必要ですか?{learnMore}(エンタープライズについて)。",
"teamHeaderLearnMore": "詳細はこちら",
"teamPerks": {
"concurrentRuns": "メンバーはワークフローを同時に実行可能",
"inviteMembers": "メンバーを招待",
"rolePermissions": "ロールベースの権限",
"sharedCreditPool": "全メンバーで共有するクレジットプール"
},
"teamPlan": {
"changePlan": "プランを変更",
"comingSoonLabel": "近日公開:",
@@ -3965,6 +3986,8 @@
"tagline": "月間クレジット数を自由に選択。多くのクレジットでさらに割引。",
"unavailable": "このチームプランは現在ご利用いただけません。"
},
"teamPlanIncludes": "{plan}のすべてに加えて、以下が含まれます:",
"teamPlanName": "チーム",
"teamWorkspace": "チームワークスペース",
"tierNameYearly": "{name} 年間",
"tiers": {

View File

@@ -325,6 +325,11 @@
"signUpWithGithub": "Github로 가입하기",
"signUpWithGoogle": "구글로 가입하기",
"title": "계정 생성"
},
"turnstile": {
"expired": "인증이 만료되었습니다. 다시 인증을 완료해 주세요.",
"failed": "인증에 실패했습니다. 다시 시도해 주세요.",
"submitBlockedHint": "회원가입을 활성화하려면 위의 인증을 완료해 주세요."
}
},
"batch": {
@@ -3742,12 +3747,14 @@
"keepSubscription": "구독 유지",
"title": "구독 취소"
},
"cancelPlan": "요금제 취소",
"cancelSuccess": "구독이 성공적으로 취소되었습니다",
"canceled": "취소됨",
"canceledCard": {
"description": "더 이상 결제되지 않습니다. {date}까지 기능을 계속 사용할 수 있습니다.",
"title": "구독이 취소되었습니다"
},
"changePlan": "요금제 변경",
"changeTo": "{plan}로 변경",
"comfyCloud": "Comfy Cloud",
"comfyCloudLogo": "Comfy Cloud 로고",
@@ -3776,6 +3783,7 @@
"paymentPageBlocked": "결제 페이지를 열 수 없습니다 — 다시 시도해 주세요",
"title": "{plan} 플랜으로 변경하시겠습니까?"
},
"endsOnDate": "{date}에 종료됩니다",
"enterprise": {
"cta": "자세히 알아보기",
"flexibility": "더 많은 유연성이나 맞춤 기능이 필요하신가요?",
@@ -3785,6 +3793,9 @@
},
"everythingInPlus": "{plan}의 모든 기능, 그리고 추가로:",
"expiresDate": "만료일 {date}",
"freePerks": {
"maxRuntime": "최대 실행 시간 {duration}"
},
"freeTier": {
"description": "무료 플랜에는 Comfy Cloud를 체험할 수 있도록 매월 {credits} 크레딧이 포함되어 있습니다.",
"descriptionGeneric": "무료 플랜에는 Comfy Cloud를 체험할 수 있는 월간 크레딧이 포함되어 있습니다.",
@@ -3812,7 +3823,7 @@
"inviteUpTo": "최대 {count}명 초대",
"invoiceHistory": "청구서 기록",
"learnMore": "더 알아보기",
"managePayment": "결제 관리",
"manageBilling": "결제 관리",
"managePlan": "플랜 관리",
"manageSubscription": "구독 관리",
"maxDuration": {
@@ -3826,7 +3837,6 @@
"maxMembersLabel": "최대 멤버 수",
"member": "멤버",
"memberCount": "{count}명 멤버",
"membersLabel": "{count}명까지 멤버",
"messageSupport": "고객 지원 문의",
"monthly": "월간",
"monthlyBonusDescription": "월간 크레딧 보너스",
@@ -3842,17 +3852,19 @@
"mostPopular": "가장 인기 있음",
"needTeamWorkspace": "팀 워크스페이스가 필요하신가요?",
"nextBillingCycle": "다음 결제 주기",
"nextMonthInvoice": "다음 달 청구서",
"outOfCreditsDescription": "생성을 계속하려면 크레딧을 추가하세요.",
"outOfCreditsTitle": "크레딧이 모두 소진되었습니다. {date}에 크레딧이 리필됩니다",
"outOfCreditsTitleNoDate": "크레딧이 모두 소진되었습니다",
"partnerNodesBalance": "\"파트너 노드\" 크레딧 잔액",
"partnerNodesCredits": "파트너 노드 크레딧",
"partnerNodesDescription": "상용/독점 모델 실행용",
"partnerNodesPricingTable": "파트너 노드 요금표",
"perMonth": "USD / 월",
"personalHeader": "개인 플랜은 개인 사용만을 위한 것입니다. {action}",
"personalHeaderAction": "팀원을 추가하려면 팀 플랜을 구독하세요.",
"personalWorkspace": "개인 워크스페이스",
"planLoadError": "요금제 정보를 불러올 수 없습니다.",
"planLoadErrorRetry": "다시 시도하기",
"planScope": {
"personal": "개인용",
"team": "팀용"
@@ -3911,11 +3923,13 @@
"pricingBlurbEnterprise": "엔터프라이즈 상담",
"pricingBlurbQuestions": "문의",
"pricingBlurbSeeDetails": "자세히 보기",
"reactivatePlan": "요금제 재활성화",
"refillsDate": "{date}에 리필",
"refillsNextCycle": "다음 주기에 리필",
"refreshCredits": "크레딧 새로고침",
"remaining": "남음",
"renewsDate": "{date}에 갱신됨",
"renewsOnDate": "{date}에 갱신됩니다",
"required": {
"pollingFailed": "구독 활성화에 실패했습니다",
"pollingSuccess": "구독이 성공적으로 활성화되었습니다!",
@@ -3930,6 +3944,7 @@
"saveYearly": "20% 절감",
"saveYearlyUpTo": "최대 20% 절약",
"soloUseOnly": "개인용 전용",
"subscribe": "구독하기",
"subscribeFailed": "구독에 실패했습니다",
"subscribeForMore": "업그레이드",
"subscribeNow": "지금 구독하기",
@@ -3949,6 +3964,12 @@
},
"teamHeader": "협업을 원하는 팀을 위한 플랜입니다. 더 많은 멤버가 필요하신가요? {learnMore} 엔터프라이즈에 대해 알아보세요.",
"teamHeaderLearnMore": "자세히 알아보기",
"teamPerks": {
"concurrentRuns": "팀원이 워크플로우를 동시에 실행 가능",
"inviteMembers": "팀원 초대",
"rolePermissions": "역할 기반 권한",
"sharedCreditPool": "모든 팀원이 공유하는 크레딧 풀"
},
"teamPlan": {
"changePlan": "플랜 변경",
"comingSoonLabel": "곧 제공:",
@@ -3965,6 +3986,8 @@
"tagline": "원하는 월간 크레딧 구독을 선택하세요. 더 많은 크레딧 구독 시 더 큰 할인 혜택을 받으세요.",
"unavailable": "이 팀 플랜은 현재 이용하실 수 없습니다."
},
"teamPlanIncludes": "{plan}의 모든 기능과 함께 다음이 포함됩니다:",
"teamPlanName": "팀",
"teamWorkspace": "팀 워크스페이스",
"tierNameYearly": "{name} 연간",
"tiers": {

View File

@@ -325,6 +325,11 @@
"signUpWithGithub": "Cadastrar-se com Github",
"signUpWithGoogle": "Cadastrar-se com Google",
"title": "Criar uma conta"
},
"turnstile": {
"expired": "Verificação expirada. Por favor, complete o desafio novamente.",
"failed": "Falha na verificação. Por favor, tente novamente.",
"submitBlockedHint": "Complete o desafio de verificação acima para habilitar o cadastro."
}
},
"batch": {
@@ -3754,12 +3759,14 @@
"keepSubscription": "Manter assinatura",
"title": "Cancelar assinatura"
},
"cancelPlan": "Cancelar plano",
"cancelSuccess": "Assinatura cancelada com sucesso",
"canceled": "Cancelado",
"canceledCard": {
"description": "Você não será mais cobrado. Seus recursos permanecem ativos até {date}.",
"title": "Sua assinatura foi cancelada"
},
"changePlan": "Alterar plano",
"changeTo": "Mudar para {plan}",
"comfyCloud": "Comfy Cloud",
"comfyCloudLogo": "Logo do Comfy Cloud",
@@ -3788,6 +3795,7 @@
"paymentPageBlocked": "Não foi possível abrir a página de pagamento — tente novamente",
"title": "Mudar para o plano {plan}?"
},
"endsOnDate": "Termina em {date}",
"enterprise": {
"cta": "Saiba mais",
"flexibility": "Procurando mais flexibilidade ou recursos personalizados?",
@@ -3797,6 +3805,9 @@
},
"everythingInPlus": "Tudo do {plan}, além de:",
"expiresDate": "Expira em {date}",
"freePerks": {
"maxRuntime": "{duration} de tempo máximo de execução"
},
"freeTier": {
"description": "Seu plano gratuito inclui {credits} créditos por mês para testar o Comfy Cloud.",
"descriptionGeneric": "Seu plano gratuito inclui uma cota mensal de créditos para testar o Comfy Cloud.",
@@ -3824,7 +3835,7 @@
"inviteUpTo": "Convide até",
"invoiceHistory": "Histórico de faturas",
"learnMore": "Saiba mais",
"managePayment": "Gerenciar pagamento",
"manageBilling": "Gerenciar cobrança",
"managePlan": "Gerenciar plano",
"manageSubscription": "Gerenciar assinatura",
"maxDuration": {
@@ -3838,7 +3849,6 @@
"maxMembersLabel": "Máx. de membros",
"member": "membro",
"memberCount": "{count} membro | {count} membros",
"membersLabel": "Até {count} membros",
"messageSupport": "Falar com o suporte",
"monthly": "Mensal",
"monthlyBonusDescription": "Bônus mensal de créditos",
@@ -3854,17 +3864,19 @@
"mostPopular": "Mais popular",
"needTeamWorkspace": "Precisa de um espaço de trabalho em equipe?",
"nextBillingCycle": "próximo ciclo de cobrança",
"nextMonthInvoice": "Fatura do próximo mês",
"outOfCreditsDescription": "Adicione mais créditos para continuar gerando.",
"outOfCreditsTitle": "Você está sem créditos. Recarga em {date}",
"outOfCreditsTitleNoDate": "Você está sem créditos",
"partnerNodesBalance": "Saldo de Créditos \"Partner Nodes\"",
"partnerNodesCredits": "Preços dos Partner Nodes",
"partnerNodesDescription": "Para executar modelos comerciais/proprietários",
"partnerNodesPricingTable": "Tabela de preços dos Partner Nodes",
"perMonth": "/ mês",
"personalHeader": "Planos pessoais são apenas para uso individual. {action}",
"personalHeaderAction": "Para adicionar colegas, assine o plano de equipe.",
"personalWorkspace": "Espaço de Trabalho Pessoal",
"planLoadError": "Não foi possível carregar os detalhes do seu plano.",
"planLoadErrorRetry": "Tentar novamente",
"planScope": {
"personal": "Para uso pessoal",
"team": "Para equipes"
@@ -3923,11 +3935,13 @@
"pricingBlurbEnterprise": "discussões enterprise",
"pricingBlurbQuestions": "dúvidas",
"pricingBlurbSeeDetails": "ver detalhes",
"reactivatePlan": "Reativar plano",
"refillsDate": "Recargas em {date}",
"refillsNextCycle": "Recargas no próximo ciclo",
"refreshCredits": "Atualizar créditos",
"remaining": "restante",
"renewsDate": "Renova em {date}",
"renewsOnDate": "Renova em {date}",
"required": {
"pollingFailed": "Falha ao ativar a assinatura",
"pollingSuccess": "Assinatura ativada com sucesso!",
@@ -3942,6 +3956,7 @@
"saveYearly": "Economize 20%",
"saveYearlyUpTo": "Economize até 20%",
"soloUseOnly": "Apenas para uso individual",
"subscribe": "Assinar",
"subscribeFailed": "Falha ao assinar",
"subscribeForMore": "Fazer upgrade",
"subscribeNow": "Assine Agora",
@@ -3961,6 +3976,12 @@
},
"teamHeader": "Para equipes que desejam colaborar. Precisa de mais membros? {learnMore} sobre enterprise.",
"teamHeaderLearnMore": "Saiba mais",
"teamPerks": {
"concurrentRuns": "Membros podem executar fluxos de trabalho simultaneamente",
"inviteMembers": "Convidar membros",
"rolePermissions": "Permissões baseadas em função",
"sharedCreditPool": "Créditos compartilhados entre todos os membros"
},
"teamPlan": {
"changePlan": "Mudar plano",
"comingSoonLabel": "Em breve:",
@@ -3977,6 +3998,8 @@
"tagline": "Escolha sua própria assinatura mensal de créditos. Obtenha um desconto maior com uma assinatura de mais créditos.",
"unavailable": "Este plano de equipe não está disponível no momento."
},
"teamPlanIncludes": "Seu plano inclui tudo do {plan}, além de:",
"teamPlanName": "Equipe",
"teamWorkspace": "Espaço de Trabalho em Equipe",
"tierNameYearly": "{name} Anual",
"tiers": {

View File

@@ -325,6 +325,11 @@
"signUpWithGithub": "Зарегистрироваться через Github",
"signUpWithGoogle": "Зарегистрироваться через Google",
"title": "Создать аккаунт"
},
"turnstile": {
"expired": "Срок действия проверки истек. Пожалуйста, выполните проверку снова.",
"failed": "Проверка не пройдена. Пожалуйста, попробуйте еще раз.",
"submitBlockedHint": "Завершите проверку выше, чтобы включить регистрацию."
}
},
"batch": {
@@ -3742,12 +3747,14 @@
"keepSubscription": "Сохранить подписку",
"title": "Отмена подписки"
},
"cancelPlan": "Отменить тариф",
"cancelSuccess": "Подписка успешно отменена",
"canceled": "Отменено",
"canceledCard": {
"description": "С вас больше не будет взиматься плата. Ваши функции останутся активными до {date}.",
"title": "Ваша подписка отменена"
},
"changePlan": "Сменить тариф",
"changeTo": "Перейти на {plan}",
"comfyCloud": "Comfy Cloud",
"comfyCloudLogo": "Логотип Comfy Cloud",
@@ -3776,6 +3783,7 @@
"paymentPageBlocked": "Не удалось открыть страницу оплаты — попробуйте еще раз",
"title": "Перейти на тариф {plan}?"
},
"endsOnDate": "Заканчивается {date}",
"enterprise": {
"cta": "Узнать больше",
"flexibility": "Нужна большая гибкость или индивидуальные функции?",
@@ -3785,6 +3793,9 @@
},
"everythingInPlus": "Всё в {plan}, плюс:",
"expiresDate": "Истекает {date}",
"freePerks": {
"maxRuntime": "Максимальное время выполнения: {duration}"
},
"freeTier": {
"description": "Ваш бесплатный тариф включает {credits} кредитов каждый месяц для использования Comfy Cloud.",
"descriptionGeneric": "Ваш бесплатный тариф включает ежемесячный лимит кредитов для использования Comfy Cloud.",
@@ -3812,7 +3823,7 @@
"inviteUpTo": "Пригласить до",
"invoiceHistory": "История счетов",
"learnMore": "Узнать больше",
"managePayment": "Управление оплатой",
"manageBilling": "Управление оплатой",
"managePlan": "Управление планом",
"manageSubscription": "Управление подпиской",
"maxDuration": {
@@ -3826,7 +3837,6 @@
"maxMembersLabel": "Макс. участников",
"member": "участник",
"memberCount": "{count} участник | {count} участников",
"membersLabel": "До {count} участников",
"messageSupport": "Написать в поддержку",
"monthly": "Ежемесячно",
"monthlyBonusDescription": "Ежемесячный бонус кредитов",
@@ -3842,17 +3852,19 @@
"mostPopular": "Самый популярный",
"needTeamWorkspace": "Нужно командное рабочее пространство?",
"nextBillingCycle": "следующий платёжный цикл",
"nextMonthInvoice": "Счет на следующий месяц",
"outOfCreditsDescription": "Добавьте кредиты, чтобы продолжить генерацию.",
"outOfCreditsTitle": "Кредиты закончились. Пополнение {date}",
"outOfCreditsTitleNoDate": "Кредиты закончились",
"partnerNodesBalance": "Баланс кредитов \"Партнёрских узлов\"",
"partnerNodesCredits": "Кредиты партнёрских узлов",
"partnerNodesDescription": "Для запуска коммерческих/проприетарных моделей",
"partnerNodesPricingTable": "Таблица цен на Partner Nodes",
"perMonth": "USD / месяц",
"personalHeader": "Личные тарифы предназначены только для индивидуального использования. {action}",
"personalHeaderAction": "Чтобы добавить участников, оформите командный тариф.",
"personalWorkspace": "Личное рабочее пространство",
"planLoadError": "Не удалось загрузить детали вашего тарифа.",
"planLoadErrorRetry": "Попробовать снова",
"planScope": {
"personal": "Для личного использования",
"team": "Для команд"
@@ -3911,11 +3923,13 @@
"pricingBlurbEnterprise": "корпоративным обсуждениям",
"pricingBlurbQuestions": "вопросам",
"pricingBlurbSeeDetails": "подробнее",
"reactivatePlan": "Возобновить тариф",
"refillsDate": "Пополнение {date}",
"refillsNextCycle": "Пополнение в следующем цикле",
"refreshCredits": "Обновить кредиты",
"remaining": "осталось",
"renewsDate": "Обновляется {date}",
"renewsOnDate": "Продлевается {date}",
"required": {
"pollingFailed": "Не удалось активировать подписку",
"pollingSuccess": "Подписка успешно активирована!",
@@ -3930,6 +3944,7 @@
"saveYearly": "Экономия 20%",
"saveYearlyUpTo": "Экономьте до 20%",
"soloUseOnly": "Только для индивидуального использования",
"subscribe": "Подписаться",
"subscribeFailed": "Не удалось подписаться",
"subscribeForMore": "Обновить",
"subscribeNow": "Подписаться сейчас",
@@ -3949,6 +3964,12 @@
},
"teamHeader": "Для команд, желающих сотрудничать. Нужно больше участников? {learnMore} о корпоративных возможностях.",
"teamHeaderLearnMore": "Узнать больше",
"teamPerks": {
"concurrentRuns": "Участники могут запускать рабочие процессы одновременно",
"inviteMembers": "Приглашение участников",
"rolePermissions": "Разрешения на основе ролей",
"sharedCreditPool": "Общий кредитный пул для всех участников"
},
"teamPlan": {
"changePlan": "Сменить тариф",
"comingSoonLabel": "Скоро появится:",
@@ -3965,6 +3986,8 @@
"tagline": "Выберите собственную ежемесячную подписку на кредиты. Чем больше кредитов — тем больше скидка.",
"unavailable": "Этот командный план сейчас недоступен."
},
"teamPlanIncludes": "Ваш тариф включает всё из {plan}, а также:",
"teamPlanName": "Команда",
"teamWorkspace": "Командное рабочее пространство",
"tierNameYearly": "{name} Ежегодно",
"tiers": {

View File

@@ -325,6 +325,11 @@
"signUpWithGithub": "Github ile kaydol",
"signUpWithGoogle": "Google ile kaydol",
"title": "Hesap oluşturun"
},
"turnstile": {
"expired": "Doğrulamanın süresi doldu. Lütfen doğrulama işlemini tekrar tamamlayın.",
"failed": "Doğrulama başarısız oldu. Lütfen tekrar deneyin.",
"submitBlockedHint": "Kayıt olmayı etkinleştirmek için yukarıdaki doğrulama işlemini tamamlayın."
}
},
"batch": {
@@ -3742,12 +3747,14 @@
"keepSubscription": "Aboneliği sürdür",
"title": "Aboneliği iptal et"
},
"cancelPlan": "Planı iptal et",
"cancelSuccess": "Abonelik başarıyla iptal edildi",
"canceled": "İptal edildi",
"canceledCard": {
"description": "Tekrar ücretlendirilmeyeceksiniz. Özellikleriniz {date} tarihine kadar aktif kalacak.",
"title": "Aboneliğiniz iptal edildi"
},
"changePlan": "Planı değiştir",
"changeTo": "{plan} planına geç",
"comfyCloud": "Comfy Cloud",
"comfyCloudLogo": "Comfy Cloud Logosu",
@@ -3776,6 +3783,7 @@
"paymentPageBlocked": "Ödeme sayfasıılamadı — lütfen tekrar deneyin",
"title": "{plan} planına geçilsin mi?"
},
"endsOnDate": "{date} tarihinde sona erecek",
"enterprise": {
"cta": "Daha fazla bilgi",
"flexibility": "Daha fazla esneklik veya özel özellikler mi arıyorsunuz?",
@@ -3785,6 +3793,9 @@
},
"everythingInPlus": "{plan} planındaki her şey ve ayrıca:",
"expiresDate": "{date} tarihinde sona erer",
"freePerks": {
"maxRuntime": "Maksimum çalışma süresi: {duration}"
},
"freeTier": {
"description": "Ücretsiz planınız, Comfy Cloud'u denemek için her ay {credits} kredi içerir.",
"descriptionGeneric": "Ücretsiz planınız, Comfy Cloud'u denemek için aylık kredi hakkı içerir.",
@@ -3812,7 +3823,7 @@
"inviteUpTo": "Şu kadar kişiyi davet et:",
"invoiceHistory": "Fatura geçmişi",
"learnMore": "Daha fazla bilgi edinin",
"managePayment": "Ödemeyi Yönet",
"manageBilling": "Faturalamayı yönet",
"managePlan": "Planı yönet",
"manageSubscription": "Aboneliği yönet",
"maxDuration": {
@@ -3826,7 +3837,6 @@
"maxMembersLabel": "Azami üye",
"member": "üye",
"memberCount": "{count} üye",
"membersLabel": "{count} üye'ye kadar",
"messageSupport": "Destek ekibine mesaj gönder",
"monthly": "Aylık",
"monthlyBonusDescription": "Aylık kredi bonusu",
@@ -3842,17 +3852,19 @@
"mostPopular": "En popüler",
"needTeamWorkspace": "Takım çalışma alanına mı ihtiyacınız var?",
"nextBillingCycle": "sonraki fatura döngüsü",
"nextMonthInvoice": "Gelecek ay faturası",
"outOfCreditsDescription": "Devam etmek için daha fazla kredi ekleyin.",
"outOfCreditsTitle": "Kredileriniz tükendi. {date} tarihinde yenilenecek",
"outOfCreditsTitleNoDate": "Kredileriniz tükendi",
"partnerNodesBalance": "\"Partner Düğümleri\" Kredi Bakiyesi",
"partnerNodesCredits": "Partner Düğümleri kredileri",
"partnerNodesDescription": "Ticari/özel modelleri çalıştırmak için",
"partnerNodesPricingTable": "Partner Node fiyat tablosu",
"perMonth": "USD / ay",
"personalHeader": "Bireysel planlar yalnızca kişisel kullanım içindir. {action}",
"personalHeaderAction": "Ekip arkadaşları eklemek için ekip planına abone olun.",
"personalWorkspace": "Kişisel Çalışma Alanı",
"planLoadError": "Plan detaylarınızı yükleyemedik.",
"planLoadErrorRetry": "Tekrar dene",
"planScope": {
"personal": "Bireysel Kullanım İçin",
"team": "Ekipler İçin"
@@ -3911,11 +3923,13 @@
"pricingBlurbEnterprise": "kurumsal görüşmeler",
"pricingBlurbQuestions": "sorular",
"pricingBlurbSeeDetails": "detayları gör",
"reactivatePlan": "Planı yeniden etkinleştir",
"refillsDate": "{date} tarihinde yenilenecek",
"refillsNextCycle": "Sonraki döngüde yenilenecek",
"refreshCredits": "Kredileri yenile",
"remaining": "kalan",
"renewsDate": "{date} tarihinde yenilenir",
"renewsOnDate": "{date} tarihinde yenilenecek",
"required": {
"pollingFailed": "Abonelik etkinleştirme başarısız oldu",
"pollingSuccess": "Abonelik başarıyla etkinleştirildi!",
@@ -3930,6 +3944,7 @@
"saveYearly": "%20 tasarruf",
"saveYearlyUpTo": "%20'ye kadar tasarruf edin",
"soloUseOnly": "Sadece bireysel kullanım",
"subscribe": "Abone ol",
"subscribeFailed": "Abonelik başarısız oldu",
"subscribeForMore": "Yükselt",
"subscribeNow": "Hemen Abone Ol",
@@ -3949,6 +3964,12 @@
},
"teamHeader": "Birlikte çalışmak isteyen ekipler için. Daha fazla üyeye mi ihtiyacınız var? {learnMore} kurumsal hakkında.",
"teamHeaderLearnMore": "Daha fazla bilgi",
"teamPerks": {
"concurrentRuns": "Üyeler iş akışlarını eşzamanlı çalıştırabilir",
"inviteMembers": "Üyeleri davet et",
"rolePermissions": "Rol tabanlı yetkilendirme",
"sharedCreditPool": "Tüm üyeler için ortak kredi havuzu"
},
"teamPlan": {
"changePlan": "Planı değiştir",
"comingSoonLabel": "Yakında:",
@@ -3965,6 +3986,8 @@
"tagline": "Kendi aylık kredi aboneliğinizi seçin. Daha fazla krediyle daha büyük indirim elde edin.",
"unavailable": "Bu ekip planı şu anda kullanılamıyor."
},
"teamPlanIncludes": "Planınız {plan} içeriğine ek olarak şunları da kapsar:",
"teamPlanName": "Takım",
"teamWorkspace": "Takım Çalışma Alanı",
"tierNameYearly": "{name} Yıllık",
"tiers": {

View File

@@ -325,6 +325,11 @@
"signUpWithGithub": "使用 Github 註冊",
"signUpWithGoogle": "使用 Google 註冊",
"title": "建立帳戶"
},
"turnstile": {
"expired": "驗證已過期。請重新完成驗證挑戰。",
"failed": "驗證失敗。請再試一次。",
"submitBlockedHint": "請先完成上方驗證挑戰以啟用註冊。"
}
},
"batch": {
@@ -3742,12 +3747,14 @@
"keepSubscription": "保留訂閱",
"title": "取消訂閱"
},
"cancelPlan": "取消方案",
"cancelSuccess": "訂閱已成功取消",
"canceled": "已取消",
"canceledCard": {
"description": "您將不會再次被收費。您的功能將持續至 {date}。",
"title": "您的訂閱已取消"
},
"changePlan": "更改方案",
"changeTo": "切換至 {plan}",
"comfyCloud": "Comfy Cloud",
"comfyCloudLogo": "Comfy Cloud 標誌",
@@ -3776,6 +3783,7 @@
"paymentPageBlocked": "無法開啟付款頁面 — 請再試一次",
"title": "更改為 {plan} 方案?"
},
"endsOnDate": "將於 {date} 結束",
"enterprise": {
"cta": "了解更多",
"flexibility": "需要更多彈性或自訂功能?",
@@ -3785,6 +3793,9 @@
},
"everythingInPlus": "包含 {plan} 的所有內容,並加上:",
"expiresDate": "將於 {date} 到期",
"freePerks": {
"maxRuntime": "最長執行時間 {duration}"
},
"freeTier": {
"description": "您的免費方案每月包含 {credits} 點數,可體驗 Comfy Cloud。",
"descriptionGeneric": "您的免費方案每月包含點數額度,可體驗 Comfy Cloud。",
@@ -3812,7 +3823,7 @@
"inviteUpTo": "可邀請最多",
"invoiceHistory": "發票記錄",
"learnMore": "了解更多",
"managePayment": "管理付款",
"manageBilling": "管理帳單",
"managePlan": "管理方案",
"manageSubscription": "管理訂閱",
"maxDuration": {
@@ -3826,7 +3837,6 @@
"maxMembersLabel": "最大成員數",
"member": "成員",
"memberCount": "{count} 位成員",
"membersLabel": "最多 {count} 位成員",
"messageSupport": "聯繫客服",
"monthly": "每月",
"monthlyBonusDescription": "每月點數獎勵",
@@ -3842,17 +3852,19 @@
"mostPopular": "最受歡迎",
"needTeamWorkspace": "需要團隊工作區?",
"nextBillingCycle": "下個計費週期",
"nextMonthInvoice": "下月發票",
"outOfCreditsDescription": "請新增點數以繼續產生。",
"outOfCreditsTitle": "您的點數已用完。點數將於 {date} 補充",
"outOfCreditsTitleNoDate": "您的點數已用完",
"partnerNodesBalance": "「合作夥伴節點」點數餘額",
"partnerNodesCredits": "合作節點點數",
"partnerNodesDescription": "用於執行商業/專有模型",
"partnerNodesPricingTable": "合作夥伴節點價格表",
"perMonth": "美元 / 月",
"personalHeader": "個人方案僅供個人使用。{action}",
"personalHeaderAction": "如需新增團隊成員,請訂閱團隊方案。",
"personalWorkspace": "個人工作區",
"planLoadError": "無法載入您的方案詳情。",
"planLoadErrorRetry": "再試一次",
"planScope": {
"personal": "個人使用",
"team": "團隊使用"
@@ -3911,11 +3923,13 @@
"pricingBlurbEnterprise": "企業洽談",
"pricingBlurbQuestions": "問題諮詢",
"pricingBlurbSeeDetails": "查看詳情",
"reactivatePlan": "重新啟用方案",
"refillsDate": "補充 {date}",
"refillsNextCycle": "下個週期補充",
"refreshCredits": "刷新點數",
"remaining": "剩餘",
"renewsDate": "將於 {date} 續訂",
"renewsOnDate": "將於 {date} 續訂",
"required": {
"pollingFailed": "訂閱啟用失敗",
"pollingSuccess": "訂閱已成功啟用!",
@@ -3930,6 +3944,7 @@
"saveYearly": "節省 20%",
"saveYearlyUpTo": "最高可省 20%",
"soloUseOnly": "僅限個人使用",
"subscribe": "訂閱",
"subscribeFailed": "訂閱失敗",
"subscribeForMore": "升級",
"subscribeNow": "立即訂閱",
@@ -3949,6 +3964,12 @@
},
"teamHeader": "適合需要協作的團隊。需要更多成員嗎?{learnMore} 企業方案。",
"teamHeaderLearnMore": "了解更多",
"teamPerks": {
"concurrentRuns": "成員可同時執行多個 workflow",
"inviteMembers": "邀請成員",
"rolePermissions": "基於角色的權限管理",
"sharedCreditPool": "全體成員共享點數池"
},
"teamPlan": {
"changePlan": "更改方案",
"comingSoonLabel": "即將推出:",
@@ -3965,6 +3986,8 @@
"tagline": "自選每月額度訂閱。額度越高,折扣越多。",
"unavailable": "此團隊方案目前無法使用。"
},
"teamPlanIncludes": "您的方案包含 {plan} 的所有內容,並額外提供:",
"teamPlanName": "團隊",
"teamWorkspace": "團隊工作區",
"tierNameYearly": "{name} 年度方案",
"tiers": {

View File

@@ -325,6 +325,11 @@
"signUpWithGithub": "使用Github注册",
"signUpWithGoogle": "使用Google注册",
"title": "创建一个账户"
},
"turnstile": {
"expired": "验证已过期。请重新完成验证。",
"failed": "验证失败。请重试。",
"submitBlockedHint": "请先完成上方的验证挑战以启用注册。"
}
},
"batch": {
@@ -3754,12 +3759,14 @@
"keepSubscription": "保留订阅",
"title": "取消订阅"
},
"cancelPlan": "取消套餐",
"cancelSuccess": "订阅取消成功",
"canceled": "已取消",
"canceledCard": {
"description": "您将不再被扣费。您的功能将在 {date} 前保持激活。",
"title": "您的订阅已被取消"
},
"changePlan": "更改套餐",
"changeTo": "更改为 {plan}",
"comfyCloud": "Comfy 云",
"comfyCloudLogo": "Comfy Cloud Logo",
@@ -3788,6 +3795,7 @@
"paymentPageBlocked": "无法打开支付页面 — 请重试",
"title": "切换到 {plan} 方案?"
},
"endsOnDate": "将于 {date} 结束",
"enterprise": {
"cta": "了解更多",
"flexibility": "需要更多灵活性或定制功能?",
@@ -3797,6 +3805,9 @@
},
"everythingInPlus": "包含 {plan} 的所有内容,并额外提供:",
"expiresDate": "于 {date} 过期",
"freePerks": {
"maxRuntime": "最长运行时长 {duration}"
},
"freeTier": {
"description": "您的免费套餐每月包含 {credits} 积分,可体验 Comfy Cloud。",
"descriptionGeneric": "您的免费套餐每月包含积分额度,可体验 Comfy Cloud。",
@@ -3824,7 +3835,7 @@
"inviteUpTo": "可邀请多达",
"invoiceHistory": "发票历史",
"learnMore": "了解更多",
"managePayment": "管理付款",
"manageBilling": "管理账单",
"managePlan": "管理订阅",
"manageSubscription": "管理订阅",
"maxDuration": {
@@ -3838,7 +3849,6 @@
"maxMembersLabel": "最大成员数",
"member": "成员",
"memberCount": "{count} 名成员",
"membersLabel": "最多 {count} 名成员",
"messageSupport": "消息支持",
"monthly": "月度",
"monthlyBonusDescription": "每月积分奖励",
@@ -3854,17 +3864,19 @@
"mostPopular": "最受欢迎",
"needTeamWorkspace": "需要团队工作区?",
"nextBillingCycle": "下一个计费周期",
"nextMonthInvoice": "下月账单",
"outOfCreditsDescription": "请添加更多点数以继续生成。",
"outOfCreditsTitle": "您的点数已用完。点数将于 {date} 补充",
"outOfCreditsTitleNoDate": "您的点数已用完",
"partnerNodesBalance": "\"合作伙伴节点\"积分余额",
"partnerNodesCredits": "合作伙伴节点积分",
"partnerNodesDescription": "用于运行商业/专有模型",
"partnerNodesPricingTable": "合作节点价格表",
"perMonth": "美元 / 月",
"personalHeader": "个人方案仅限个人使用。{action}",
"personalHeaderAction": "如需添加团队成员,请订阅团队方案。",
"personalWorkspace": "个人工作区",
"planLoadError": "无法加载您的套餐详情。",
"planLoadErrorRetry": "重试",
"planScope": {
"personal": "个人使用",
"team": "团队使用"
@@ -3923,11 +3935,13 @@
"pricingBlurbEnterprise": "企业合作",
"pricingBlurbQuestions": "问题咨询",
"pricingBlurbSeeDetails": "查看详情",
"reactivatePlan": "重新激活套餐",
"refillsDate": "补充 {date}",
"refillsNextCycle": "下个周期补充",
"refreshCredits": "刷新额度",
"remaining": "剩余",
"renewsDate": "将于 {date} 续订",
"renewsOnDate": "将于 {date} 自动续订",
"required": {
"pollingFailed": "订阅激活失败",
"pollingSuccess": "订阅激活成功!",
@@ -3942,6 +3956,7 @@
"saveYearly": "立省 20%",
"saveYearlyUpTo": "最高可节省 20%",
"soloUseOnly": "仅限个人使用",
"subscribe": "订阅",
"subscribeFailed": "订阅失败",
"subscribeForMore": "升级",
"subscribeNow": "立即订阅",
@@ -3961,6 +3976,12 @@
},
"teamHeader": "适合希望协作的团队。需要更多成员?{learnMore} 了解企业版。",
"teamHeaderLearnMore": "了解更多",
"teamPerks": {
"concurrentRuns": "成员可同时运行工作流",
"inviteMembers": "邀请成员",
"rolePermissions": "基于角色的权限",
"sharedCreditPool": "所有成员共享积分池"
},
"teamPlan": {
"changePlan": "更改方案",
"comingSoonLabel": "即将上线:",
@@ -3977,6 +3998,8 @@
"tagline": "自定义每月积分订阅。订阅更多积分可享更大折扣。",
"unavailable": "该团队套餐当前不可用。"
},
"teamPlanIncludes": "您的套餐包含 {plan} 的所有内容,以及:",
"teamPlanName": "团队",
"teamWorkspace": "团队工作区",
"tierNameYearly": "{name} 年度",
"tiers": {

View File

@@ -2,7 +2,7 @@ import { createSharedComposable, whenever } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { toValue } from 'vue'
import type { LGraphGroup } from '@/lib/litegraph/src/LGraphGroup'
import type { Positionable } from '@/lib/litegraph/src/interfaces'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { AutoPanController } from '@/renderer/core/canvas/useAutoPan'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
@@ -16,7 +16,7 @@ import type {
import { useNodeSnap } from '@/renderer/extensions/vueNodes/composables/useNodeSnap'
import { useShiftKeySync } from '@/renderer/extensions/vueNodes/composables/useShiftKeySync'
import { useTransformState } from '@/renderer/core/layout/transform/useTransformState'
import { isLGraphGroup } from '@/utils/litegraphUtil'
import { isLGraphNode } from '@/utils/litegraphUtil'
export const useNodeDrag = createSharedComposable(useNodeDragIndividual)
@@ -44,7 +44,7 @@ function useNodeDragIndividual() {
// For groups: track the last applied canvas delta to compute frame delta
let lastCanvasDelta: Point | null = null
let selectedGroups: LGraphGroup[] | null = null
let selectedNonNode: Positionable[] | null = null
// Auto-pan state
let autoPan: AutoPanController | null = null
@@ -90,10 +90,10 @@ function useNodeDragIndividual() {
// Capture selected groups only if the dragged node is part of the selection
// This prevents groups from moving when dragging an unrelated node
if (isDraggedNodeInSelection) {
selectedGroups = toValue(selectedItems).filter(isLGraphGroup)
selectedNonNode = toValue(selectedItems).filter((i) => !isLGraphNode(i))
lastCanvasDelta = { x: 0, y: 0 }
} else {
selectedGroups = null
selectedNonNode = null
lastCanvasDelta = null
}
@@ -123,8 +123,8 @@ function useNodeDragIndividual() {
pos.y += panY
}
}
if (selectedGroups) {
for (const group of selectedGroups) {
if (selectedNonNode) {
for (const group of selectedNonNode) {
group.move(panX, panY, true)
}
}
@@ -182,13 +182,13 @@ function useNodeDragIndividual() {
mutations.batchMoveNodes(updates)
if (selectedGroups && selectedGroups.length > 0 && lastCanvasDelta) {
if (selectedNonNode && selectedNonNode.length > 0 && lastCanvasDelta) {
const frameDelta = {
x: canvasDelta.x - lastCanvasDelta.x,
y: canvasDelta.y - lastCanvasDelta.y
}
for (const group of selectedGroups) {
for (const group of selectedNonNode) {
group.move(frameDelta.x, frameDelta.y, true)
}
}
@@ -289,7 +289,7 @@ function useNodeDragIndividual() {
dragStartPos = null
dragStartMouse = null
otherSelectedNodesStartPositions = null
selectedGroups = null
selectedNonNode = null
lastCanvasDelta = null
autoPan?.stop()

View File

@@ -0,0 +1,34 @@
<template>
<div class="col-span-2 flex justify-start">
<Button
class="border-0 bg-component-node-widget-background px-2 py-1 text-base-foreground"
:disabled="widget.options?.disabled"
size="sm"
variant="textonly"
@click="handleClick"
>
<span
class="mr-1 icon-[material-symbols--add] size-4"
aria-hidden="true"
/>
{{ t('dynamicGroup.addRow') }}
</Button>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
const { widget } = defineProps<{
widget: SimplifiedWidget<number>
}>()
const { t } = useI18n()
function handleClick() {
widget.callback?.(widget.value)
}
</script>

View File

@@ -0,0 +1,43 @@
<template>
<div
class="border-node-slot-background col-span-2 flex items-center justify-between border-t pt-1"
>
<span class="text-xs font-medium text-base-foreground/70">
{{ rowLabel }}
</span>
<button
v-if="widget.options?.removable"
class="hover:text-danger rounded-sm p-0.5 text-base-foreground/50 transition-colors"
:aria-label="t('dynamicGroup.removeRow')"
@click="handleRemove"
>
<span
class="icon-[material-symbols--close] size-3.5"
aria-hidden="true"
/>
</button>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
const { widget } = defineProps<{
widget: SimplifiedWidget<number>
}>()
const { t } = useI18n()
const rowLabel = computed(() => {
const match = /__row__(\d+)$/.exec(widget.name)
const index = match ? Number(match[1]) : 0
return t('dynamicGroup.row', { index: index + 1 })
})
function handleRemove() {
widget.callback?.(widget.value)
}
</script>

View File

@@ -75,6 +75,14 @@ const WidgetBoundingBoxes = defineAsyncComponent(
const WidgetColors = defineAsyncComponent(
() => import('@/components/palette/WidgetColors.vue')
)
const WidgetDynamicGroupAdd = defineAsyncComponent(
() =>
import('@/renderer/extensions/vueNodes/widgets/components/WidgetDynamicGroupAdd.vue')
)
const WidgetDynamicGroupRow = defineAsyncComponent(
() =>
import('@/renderer/extensions/vueNodes/widgets/components/WidgetDynamicGroupRow.vue')
)
export const FOR_TESTING = {
WidgetButton,
@@ -241,6 +249,22 @@ const coreWidgetDefinitions: Array<[string, WidgetDefinition]> = [
aliases: ['COLORS'],
essential: false
}
],
[
'dynamic_group_add',
{
component: WidgetDynamicGroupAdd,
aliases: ['COMFY_DYNAMICGROUP_V3'],
essential: false
}
],
[
'dynamic_group_row',
{
component: WidgetDynamicGroupRow,
aliases: [],
essential: false
}
]
]

View File

@@ -343,6 +343,30 @@ class NodeWithPriceBadge(IO.ComfyNode):
async def execute(cls, price):
return IO.NodeOutput()
class NodeWithDynamicCombo(IO.ComfyNode):
@classmethod
def define_schema(cls):
return IO.Schema(
node_id="DevToolsNodeWithDynamicCombo",
display_name="Node With Dynamic Combo",
description="A node with a Dynamic combo",
inputs=[IO.DynamicCombo.Input("combo", options=[
IO.DynamicCombo.Option("option1", [IO.Combo.Input("suboption", options=["1x"])]),
IO.DynamicCombo.Option("option2", [IO.Combo.Input("suboption", options=["2x"])]),
IO.DynamicCombo.Option("option3", [IO.Image.Input("image")]),
IO.DynamicCombo.Option("option4", [
IO.DynamicCombo.Input("subcombo", options=[
IO.DynamicCombo.Option("opt1", [IO.Float.Input("float_x"), IO.Float.Input("float_y")]),
IO.DynamicCombo.Option("opt2", [IO.Mask.Input("mask1", optional=True)]),
])
])]
)],
)
@classmethod
async def execute(cls):
return IO.NodeOutput()
NODE_CLASS_MAPPINGS = {
"DevToolsLongComboDropdown": LongComboDropdown,
@@ -361,6 +385,7 @@ NODE_CLASS_MAPPINGS = {
"DevToolsNodeWithV2ComboInput": NodeWithV2ComboInput,
"DevToolsNodeWithLegacyWidget": NodeWithLegacyWidget,
"DevToolsNodeWithPriceBadge": NodeWithPriceBadge,
"DevToolsNodeWithDynamicCombo": NodeWithDynamicCombo,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -380,6 +405,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"DevToolsNodeWithV2ComboInput": "Node With V2 Combo Input",
"DevToolsNodeWithLegacyWidget": "Node With Legacy Widget",
"DevToolsNodeWithPriceBadge": "Node With Price Badge",
"DevToolsNodeWithDynamicCombo": "Node With Dynamic Combo",
}
__all__ = [