mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-26 17:47:17 +00:00
Compare commits
5 Commits
v1.47.4
...
DynamicGro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a16d488838 | ||
|
|
13b42d9b59 | ||
|
|
e604c85b88 | ||
|
|
7ae3ad936c | ||
|
|
842e3d7541 |
@@ -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' },
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ?? {}
|
||||
|
||||
|
||||
@@ -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'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -71,6 +71,7 @@ export interface IWidgetOptions<TValues = unknown> {
|
||||
|
||||
// Vue widget options
|
||||
disabled?: boolean
|
||||
removable?: boolean
|
||||
useGrouping?: boolean
|
||||
placeholder?: string
|
||||
showThumbnails?: boolean
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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*",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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 l’abonnement",
|
||||
"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 d’abonnement",
|
||||
"changeTo": "Changer pour {plan}",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyCloudLogo": "Logo Comfy Cloud",
|
||||
@@ -3776,6 +3783,7 @@
|
||||
"paymentPageBlocked": "Impossible d’ouvrir 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 d’exé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 d’un 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 l’abonnement",
|
||||
"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": "S’abonner",
|
||||
"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 l’offre 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 d’une 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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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ı açı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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
@@ -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__ = [
|
||||
|
||||
Reference in New Issue
Block a user