mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-18 17:58:23 +00:00
Compare commits
4 Commits
feat/mask-
...
codex/code
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81332a3dca | ||
|
|
d4b1ced906 | ||
|
|
d791351a1c | ||
|
|
922be31b74 |
@@ -83,8 +83,8 @@ const translations = {
|
||||
'如果您是 ComfyUI 新手,可以从 App 模式开始——这是工作流的简化视图。您随时可以切换回节点图视图以深入了解。'
|
||||
},
|
||||
'showcase.feature3.title': {
|
||||
en: 'Community Workflows on Comfy Hub',
|
||||
'zh-CN': 'Comfy Hub 上的社区工作流'
|
||||
en: 'Community Workflows on Comfy Workflows',
|
||||
'zh-CN': 'Comfy Workflows 上的社区工作流'
|
||||
},
|
||||
'showcase.feature3.description': {
|
||||
en: 'Browse and remix thousands of community-shared workflows. Start from a proven template and customize it to your needs.',
|
||||
@@ -2272,7 +2272,7 @@ const translations = {
|
||||
en: 'Comfy Enterprise',
|
||||
'zh-CN': 'Comfy 企业版'
|
||||
},
|
||||
'nav.comfyHub': { en: 'Comfy Hub', 'zh-CN': 'Comfy Hub' },
|
||||
'nav.comfyHub': { en: 'Comfy Workflows', 'zh-CN': 'Comfy Workflows' },
|
||||
'nav.gallery': { en: 'Gallery', 'zh-CN': '画廊' },
|
||||
'nav.learning': { en: 'Learning', 'zh-CN': '学习' },
|
||||
'nav.blogs': { en: 'Blog', 'zh-CN': '博客' },
|
||||
@@ -4302,8 +4302,8 @@ const translations = {
|
||||
'zh-CN': '{count} 个工作流'
|
||||
},
|
||||
'models.list.contact.label': {
|
||||
en: 'COMFY HUB',
|
||||
'zh-CN': 'COMFY HUB'
|
||||
en: 'COMFY WORKFLOWS',
|
||||
'zh-CN': 'COMFY WORKFLOWS'
|
||||
},
|
||||
'models.showcase.label': { en: 'AI MODELS', 'zh-CN': 'AI 模型' },
|
||||
'models.showcase.heading': {
|
||||
@@ -4339,9 +4339,9 @@ const translations = {
|
||||
'zh-CN': 'Wan 2.2\n文字转视频'
|
||||
},
|
||||
'models.list.contact.heading': {
|
||||
en: 'Pick a model and explore what the community has built. <a href="https://comfy.org/workflows" target="_blank" rel="noopener noreferrer" class="text-primary-comfy-yellow underline">Browse Comfy Hub</a> for the newest workflows.',
|
||||
en: 'Pick a model and explore what the community has built. <a href="https://comfy.org/workflows" target="_blank" rel="noopener noreferrer" class="text-primary-comfy-yellow underline">Browse Comfy Workflows</a> for the newest workflows.',
|
||||
'zh-CN':
|
||||
'选择一个模型,浏览社区的创作成果。<a href="https://comfy.org/workflows" target="_blank" rel="noopener noreferrer" class="text-primary-comfy-yellow underline">访问 Comfy Hub</a> 查看最新工作流。'
|
||||
'选择一个模型,浏览社区的创作成果。<a href="https://comfy.org/workflows" target="_blank" rel="noopener noreferrer" class="text-primary-comfy-yellow underline">访问 Comfy Workflows</a> 查看最新工作流。'
|
||||
},
|
||||
|
||||
// Payment status pages
|
||||
|
||||
@@ -44,7 +44,7 @@ const dirDescriptions: Record<string, string> = {
|
||||
}
|
||||
|
||||
const dirDesc = dirDescriptions[model.directory] ?? 'an AI model'
|
||||
const whatIsDescription = `${displayName} is ${dirDesc}. You can run it locally in ComfyUI with full control over every parameter, or access it through Comfy Cloud. ComfyUI's node-based workflow editor lets you connect ${displayName} with ControlNets, LoRAs, upscalers, and custom nodes to build any pipeline you need. There are ${model.workflowCount} community workflow templates using ${displayName} on Comfy Hub, ready to load and customize.`
|
||||
const whatIsDescription = `${displayName} is ${dirDesc}. You can run it locally in ComfyUI with full control over every parameter, or access it through Comfy Cloud. ComfyUI's node-based workflow editor lets you connect ${displayName} with ControlNets, LoRAs, upscalers, and custom nodes to build any pipeline you need. There are ${model.workflowCount} community workflow templates using ${displayName} on Comfy Workflows, ready to load and customize.`
|
||||
|
||||
const pageTitle = `${displayName} in ComfyUI`
|
||||
const pageDescription = `Run ${displayName} in ComfyUI with full parameter control. ${model.workflowCount} community workflow templates, step-by-step tutorials, and free local inference.`
|
||||
@@ -87,7 +87,7 @@ const faqPage: JsonLdNode = {
|
||||
name: `How many ComfyUI workflows use ${displayName}?`,
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: `There are ${model.workflowCount} community workflow templates that use ${displayName} on Comfy Hub. Each template is ready to run in ComfyUI and can be customized to suit your project.`,
|
||||
text: `There are ${model.workflowCount} community workflow templates that use ${displayName} on Comfy Workflows. Each template is ready to run in ComfyUI and can be customized to suit your project.`,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
coverage:
|
||||
status:
|
||||
patch:
|
||||
default:
|
||||
informational: true
|
||||
|
||||
comment:
|
||||
layout: 'header, diff, flags, files'
|
||||
behavior: default
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
--color-gold-500: #fdab34;
|
||||
--color-gold-600: #fd9903;
|
||||
|
||||
--color-credit: #fabc25;
|
||||
--color-credit: #ddbd31;
|
||||
|
||||
--color-coral-500: #f75951;
|
||||
--color-coral-600: #e04e48;
|
||||
@@ -140,26 +140,6 @@
|
||||
--button-surface: var(--color-white);
|
||||
--button-surface-contrast: var(--color-black);
|
||||
|
||||
--subscription-button-gradient:
|
||||
linear-gradient(
|
||||
315deg,
|
||||
rgb(105 230 255 / 0.15) 0%,
|
||||
rgb(99 73 233 / 0.5) 100%
|
||||
),
|
||||
radial-gradient(
|
||||
70.71% 70.71% at 50% 50%,
|
||||
rgb(62 99 222 / 0.15) 0.01%,
|
||||
rgb(66 0 123 / 0.5) 100%
|
||||
),
|
||||
linear-gradient(
|
||||
92deg,
|
||||
#d000ff 0.38%,
|
||||
#b009fe 37.07%,
|
||||
#3e1ffc 65.17%,
|
||||
#009dff 103.86%
|
||||
),
|
||||
linear-gradient(var(--color-button-surface, #2d2e32));
|
||||
|
||||
/* Code styling colors for help menu*/
|
||||
--code-text-color: rgb(0 122 255 / 1);
|
||||
--code-bg-color: rgb(96 165 250 / 0.2);
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<!-- Credits Section -->
|
||||
<div v-if="isActiveSubscription" class="flex items-center gap-2 px-4 py-2">
|
||||
<i class="icon-[lucide--component] text-sm text-amber-400" />
|
||||
<i class="icon-[lucide--component] text-sm text-credit" />
|
||||
<Skeleton v-if="isLoading" width="4rem" height="1.25rem" class="w-full" />
|
||||
<span v-else class="text-base font-semibold text-base-foreground">{{
|
||||
formattedBalance
|
||||
@@ -48,7 +48,7 @@
|
||||
</Button>
|
||||
<Button
|
||||
v-if="isCloud && isFreeTier"
|
||||
variant="gradient"
|
||||
variant="subscribe"
|
||||
size="sm"
|
||||
data-testid="upgrade-to-add-credits-button"
|
||||
@click="handleUpgradeToAddCredits"
|
||||
@@ -72,7 +72,7 @@
|
||||
:fluid="false"
|
||||
:label="$t('subscription.subscribeToComfyCloud')"
|
||||
size="sm"
|
||||
button-variant="gradient"
|
||||
button-variant="subscribe"
|
||||
@subscribed="handleSubscribed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Button
|
||||
v-if="isCloud && isFreeTier"
|
||||
class="mr-2 shrink-0 whitespace-nowrap"
|
||||
variant="gradient"
|
||||
variant="subscribe"
|
||||
size="sm"
|
||||
data-testid="topbar-subscribe-button"
|
||||
@click="handleClick"
|
||||
|
||||
@@ -24,8 +24,8 @@ export const buttonVariants = cva({
|
||||
base: 'bg-base-background text-base-foreground hover:bg-secondary-background-hover',
|
||||
tertiary:
|
||||
'bg-tertiary-background text-base-foreground hover:bg-tertiary-background-hover',
|
||||
gradient:
|
||||
'border-transparent bg-(image:--subscription-button-gradient) text-white hover:opacity-90'
|
||||
subscribe:
|
||||
'border-transparent bg-credit text-charcoal-800 hover:opacity-80'
|
||||
},
|
||||
size: {
|
||||
sm: 'h-6 rounded-sm px-2 py-1 text-xs',
|
||||
@@ -58,7 +58,7 @@ const variants = [
|
||||
'base',
|
||||
'tertiary',
|
||||
'overlay-white',
|
||||
'gradient'
|
||||
'subscribe'
|
||||
] as const satisfies Array<ButtonVariants['variant']>
|
||||
const sizes = [
|
||||
'sm',
|
||||
|
||||
@@ -132,7 +132,7 @@ export function useChart(
|
||||
const chartInstance = ref<Chart | null>(null)
|
||||
|
||||
function createChart() {
|
||||
if (!canvasRef.value) return
|
||||
if (canvasRef.value === null) return
|
||||
|
||||
chartInstance.value?.destroy()
|
||||
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { previewCompressionParams } from './previewCompressionParams'
|
||||
|
||||
describe('previewCompressionParams', () => {
|
||||
it('returns no params when compression is disabled and no preview format is set', () => {
|
||||
expect(
|
||||
previewCompressionParams({
|
||||
compressionEnabled: false,
|
||||
previewFormat: '',
|
||||
maxSize: 4096
|
||||
})
|
||||
).toBe('')
|
||||
})
|
||||
|
||||
it('keeps the global preview format when compression is disabled', () => {
|
||||
expect(
|
||||
previewCompressionParams({
|
||||
compressionEnabled: false,
|
||||
previewFormat: 'jpeg;80',
|
||||
maxSize: 4096
|
||||
})
|
||||
).toBe('&preview=jpeg;80')
|
||||
})
|
||||
|
||||
it('adds max_size and a default format when compression is enabled', () => {
|
||||
expect(
|
||||
previewCompressionParams({
|
||||
compressionEnabled: true,
|
||||
previewFormat: '',
|
||||
maxSize: 4096
|
||||
})
|
||||
).toBe('&preview=webp;90&max_size=4096')
|
||||
})
|
||||
|
||||
it('respects the global preview format when compression is enabled', () => {
|
||||
expect(
|
||||
previewCompressionParams({
|
||||
compressionEnabled: true,
|
||||
previewFormat: 'jpeg;80',
|
||||
maxSize: 2048
|
||||
})
|
||||
).toBe('&preview=jpeg;80&max_size=2048')
|
||||
})
|
||||
|
||||
it('clamps out-of-range max sizes into the 512-8192 range', () => {
|
||||
expect(
|
||||
previewCompressionParams({
|
||||
compressionEnabled: true,
|
||||
previewFormat: '',
|
||||
maxSize: 0
|
||||
})
|
||||
).toBe('&preview=webp;90&max_size=512')
|
||||
expect(
|
||||
previewCompressionParams({
|
||||
compressionEnabled: true,
|
||||
previewFormat: '',
|
||||
maxSize: 100000
|
||||
})
|
||||
).toBe('&preview=webp;90&max_size=8192')
|
||||
})
|
||||
|
||||
it('rounds fractional max sizes to an integer', () => {
|
||||
expect(
|
||||
previewCompressionParams({
|
||||
compressionEnabled: true,
|
||||
previewFormat: '',
|
||||
maxSize: 1536.4
|
||||
})
|
||||
).toBe('&preview=webp;90&max_size=1536')
|
||||
})
|
||||
})
|
||||
@@ -1,23 +0,0 @@
|
||||
import { clamp } from 'es-toolkit'
|
||||
|
||||
const DEFAULT_PREVIEW_FORMAT = 'webp;90'
|
||||
const MIN_PREVIEW_SIZE = 512
|
||||
const MAX_PREVIEW_SIZE = 8192
|
||||
|
||||
export function previewCompressionParams(options: {
|
||||
compressionEnabled: boolean
|
||||
previewFormat: string
|
||||
maxSize: number
|
||||
}): string {
|
||||
const { compressionEnabled, previewFormat, maxSize } = options
|
||||
if (!compressionEnabled) {
|
||||
return previewFormat ? `&preview=${previewFormat}` : ''
|
||||
}
|
||||
const format = previewFormat || DEFAULT_PREVIEW_FORMAT
|
||||
const clampedSize = clamp(
|
||||
Math.round(maxSize),
|
||||
MIN_PREVIEW_SIZE,
|
||||
MAX_PREVIEW_SIZE
|
||||
)
|
||||
return `&preview=${format}&max_size=${clampedSize}`
|
||||
}
|
||||
@@ -31,12 +31,6 @@ vi.mock('@/platform/distribution/types', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: vi.fn(() => ({
|
||||
get: vi.fn(() => false)
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
fetchApi: vi.fn(),
|
||||
|
||||
@@ -3,11 +3,9 @@ import type { ImageRef, ImageLayer } from '@/stores/maskEditorDataStore'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { parseImageWidgetValue } from '@/utils/imageUtil'
|
||||
import { previewCompressionParams } from './previewCompressionParams'
|
||||
|
||||
export function extractWidgetStringValue(value: unknown): string | undefined {
|
||||
if (typeof value === 'string') return value
|
||||
@@ -63,7 +61,7 @@ function toRef(filename: string, subfolder: string): ImageRef {
|
||||
}
|
||||
}
|
||||
|
||||
function mkFileUrl(props: { ref: ImageRef }): string {
|
||||
function mkFileUrl(props: { ref: ImageRef; preview?: boolean }): string {
|
||||
const params = new URLSearchParams()
|
||||
params.set('filename', props.ref.filename)
|
||||
if (props.ref.subfolder) {
|
||||
@@ -73,15 +71,10 @@ function mkFileUrl(props: { ref: ImageRef }): string {
|
||||
params.set('type', props.ref.type)
|
||||
}
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const pathPlusQueryParams = api.apiURL(
|
||||
'/view?' +
|
||||
params.toString() +
|
||||
previewCompressionParams({
|
||||
compressionEnabled: settingStore.get('Comfy.Image.PreviewCompression'),
|
||||
previewFormat: settingStore.get('Comfy.PreviewFormat'),
|
||||
maxSize: settingStore.get('Comfy.Image.PreviewMaxSize')
|
||||
}) +
|
||||
app.getPreviewFormatParam() +
|
||||
app.getRandParam()
|
||||
)
|
||||
const imageElement = new Image()
|
||||
|
||||
@@ -75,18 +75,6 @@ vi.mock('@/scripts/app', () => ({
|
||||
|
||||
vi.mock('@/platform/distribution/types', () => ({ isCloud: false }))
|
||||
|
||||
const mockSettings = vi.hoisted(() => ({ previewCompression: false }))
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: vi.fn(() => ({
|
||||
get: vi.fn((key: string) =>
|
||||
key === 'Comfy.Image.PreviewCompression'
|
||||
? mockSettings.previewCompression
|
||||
: undefined
|
||||
)
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||
useWorkflowStore: vi.fn(() => ({
|
||||
nodeIdToNodeLocatorId: vi.fn((id: string | number) => String(id)),
|
||||
@@ -105,7 +93,6 @@ describe('useMaskEditorSaver', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
vi.clearAllMocks()
|
||||
mockSettings.previewCompression = false
|
||||
|
||||
app.nodeOutputs = {}
|
||||
app.nodePreviewImages = {}
|
||||
@@ -214,71 +201,4 @@ describe('useMaskEditorSaver', () => {
|
||||
expect(body.get('type')).toBe('input')
|
||||
expect(body.get('subfolder')).toBeNull()
|
||||
})
|
||||
|
||||
it('composites on the server via /upload/mask when preview compression is enabled', async () => {
|
||||
mockSettings.previewCompression = true
|
||||
const fetchApiMock = vi.mocked(api.fetchApi)
|
||||
|
||||
const { save } = useMaskEditorSaver()
|
||||
await save()
|
||||
|
||||
expect(fetchApiMock).toHaveBeenCalledTimes(1)
|
||||
const [route, init] = fetchApiMock.mock.calls[0]
|
||||
expect(route).toBe('/upload/mask')
|
||||
|
||||
const body = init?.body as FormData
|
||||
expect(body).toBeInstanceOf(FormData)
|
||||
expect(body.get('original_ref')).toBe(
|
||||
JSON.stringify({ filename: 'original.png', subfolder: '', type: 'input' })
|
||||
)
|
||||
expect(body.get('image')).toBeInstanceOf(Blob)
|
||||
expect(body.get('paint')).toBeInstanceOf(Blob)
|
||||
expect(String(body.get('paint_filename'))).toMatch(/^clipspace-paint-/)
|
||||
expect(String(body.get('painted_filename'))).toMatch(/^clipspace-painted-/)
|
||||
expect(String(body.get('painted_masked_filename'))).toMatch(
|
||||
/^clipspace-painted-masked-/
|
||||
)
|
||||
})
|
||||
|
||||
it('updates layer refs from the server composite response', async () => {
|
||||
mockSettings.previewCompression = true
|
||||
|
||||
const { save } = useMaskEditorSaver()
|
||||
await save()
|
||||
|
||||
const outputData = mockDataStore.outputData as {
|
||||
maskedImage: { ref: { filename: string; subfolder: string } }
|
||||
paintedMaskedImage: { ref: { subfolder: string; type: string } }
|
||||
}
|
||||
expect(outputData.maskedImage.ref.filename).toBe(
|
||||
'clipspace-painted-masked-123.png'
|
||||
)
|
||||
expect(outputData.paintedMaskedImage.ref.subfolder).toBe('clipspace')
|
||||
expect(outputData.paintedMaskedImage.ref.type).toBe('input')
|
||||
})
|
||||
|
||||
it('throws an actionable error when the composite response is not valid JSON', async () => {
|
||||
mockSettings.previewCompression = true
|
||||
vi.mocked(api.fetchApi).mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.reject(new Error('Unexpected end of JSON input'))
|
||||
} as Response)
|
||||
|
||||
const { save } = useMaskEditorSaver()
|
||||
await expect(save()).rejects.toThrow(/Invalid composite upload response/)
|
||||
})
|
||||
|
||||
it('throws an error when the server composite request fails', async () => {
|
||||
mockSettings.previewCompression = true
|
||||
vi.mocked(api.fetchApi).mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
text: () => Promise.resolve('Internal Server Error')
|
||||
} as Response)
|
||||
|
||||
const { save } = useMaskEditorSaver()
|
||||
await expect(save()).rejects.toThrow(
|
||||
/Failed to composite mask layers on server \(500: Internal Server Error\)/
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { UploadImageResponse } from '@comfyorg/ingest-types'
|
||||
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useMaskEditorDataStore } from '@/stores/maskEditorDataStore'
|
||||
import { useMaskEditorStore } from '@/stores/maskEditorStore'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
@@ -51,11 +49,7 @@ export function useMaskEditorSaver() {
|
||||
|
||||
await updateNodePreview(sourceNode, outputData)
|
||||
|
||||
if (shouldCompositeOnServer()) {
|
||||
await uploadLayersWithServerComposite(outputData)
|
||||
} else {
|
||||
await uploadAllLayers(outputData)
|
||||
}
|
||||
await uploadAllLayers(outputData)
|
||||
|
||||
updateNodeWithServerReferences(sourceNode, outputData)
|
||||
|
||||
@@ -215,77 +209,6 @@ export function useMaskEditorSaver() {
|
||||
return { canvas, blob, ref }
|
||||
}
|
||||
|
||||
function shouldCompositeOnServer(): boolean {
|
||||
return (
|
||||
!isCloud &&
|
||||
useSettingStore().get('Comfy.Image.PreviewCompression') &&
|
||||
!!dataStore.inputData?.sourceRef
|
||||
)
|
||||
}
|
||||
|
||||
async function uploadLayersWithServerComposite(
|
||||
outputData: EditorOutputData
|
||||
): Promise<void> {
|
||||
const sourceRef = dataStore.inputData!.sourceRef
|
||||
const formData = new FormData()
|
||||
formData.append(
|
||||
'image',
|
||||
outputData.maskedImage.blob,
|
||||
outputData.maskedImage.ref.filename
|
||||
)
|
||||
formData.append('type', 'input')
|
||||
formData.append('original_ref', JSON.stringify(sourceRef))
|
||||
formData.append(
|
||||
'paint',
|
||||
outputData.paintLayer.blob,
|
||||
outputData.paintLayer.ref.filename
|
||||
)
|
||||
formData.append('paint_filename', outputData.paintLayer.ref.filename)
|
||||
formData.append('painted_filename', outputData.paintedImage.ref.filename)
|
||||
formData.append(
|
||||
'painted_masked_filename',
|
||||
outputData.paintedMaskedImage.ref.filename
|
||||
)
|
||||
|
||||
const response = await api.fetchApi('/upload/mask', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text().catch(() => '')
|
||||
throw new Error(
|
||||
`Failed to composite mask layers on server (${response.status}${body ? `: ${body}` : ''})`
|
||||
)
|
||||
}
|
||||
|
||||
let data: UploadImageResponse
|
||||
try {
|
||||
data = await response.json()
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Invalid composite upload response: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
{ cause: error }
|
||||
)
|
||||
}
|
||||
if (!data?.name) {
|
||||
throw new Error('Composite upload response missing file name')
|
||||
}
|
||||
|
||||
const subfolder = data.subfolder || ''
|
||||
const type = data.type || 'input'
|
||||
outputData.maskedImage.ref = { filename: data.name, subfolder, type }
|
||||
for (const layer of [
|
||||
outputData.paintLayer,
|
||||
outputData.paintedImage,
|
||||
outputData.paintedMaskedImage
|
||||
]) {
|
||||
layer.ref = { ...layer.ref, subfolder, type }
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadAllLayers(outputData: EditorOutputData): Promise<void> {
|
||||
const actualMaskedRef = await uploadLayer(outputData.maskedImage)
|
||||
const actualPaintRef = await uploadLayer(outputData.paintLayer)
|
||||
|
||||
@@ -210,14 +210,6 @@
|
||||
"Comfy_Locale": {
|
||||
"name": "Language"
|
||||
},
|
||||
"Comfy_Image_PreviewCompression": {
|
||||
"name": "Load large images as compressed previews",
|
||||
"tooltip": "When enabled, image editors (currently the mask editor) load images as compressed, downscaled previews. This greatly improves performance with very large images; edits are applied to the original at full resolution on save. Downscaling requires the assets system on the server (--enable-assets); without it images are only recompressed at full resolution."
|
||||
},
|
||||
"Comfy_Image_PreviewMaxSize": {
|
||||
"name": "Compressed preview maximum size",
|
||||
"tooltip": "Maximum size in pixels for the longest edge of a compressed preview. Images whose longest edge exceeds this are downscaled proportionally until it fits; the aspect ratio is preserved and images are never upscaled."
|
||||
},
|
||||
"Comfy_MaskEditor_BrushAdjustmentSpeed": {
|
||||
"name": "Brush adjustment speed multiplier",
|
||||
"tooltip": "Controls how quickly the brush size and hardness change when adjusting. Higher values mean faster changes."
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
<div v-if="showActionButton" class="flex flex-col gap-3">
|
||||
<Button
|
||||
v-if="isFreeTier"
|
||||
variant="gradient"
|
||||
variant="subscribe"
|
||||
size="lg"
|
||||
class="w-full font-normal"
|
||||
@click="handleUpgradeToAddCredits"
|
||||
|
||||
@@ -43,7 +43,7 @@ const label = computed(() =>
|
||||
v-if="hasInvalidNodes"
|
||||
class="flex w-full items-center justify-center gap-2"
|
||||
>
|
||||
<i class="icon-[comfy--credits] bg-amber-400" />
|
||||
<i class="icon-[comfy--credits] bg-credit" />
|
||||
{{ t('actionbar.freeTierPartner') }}
|
||||
</div>
|
||||
<div v-else class="flex w-full items-center justify-between">
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
</span>
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<i
|
||||
class="icon-[comfy--credits] size-4 shrink-0 bg-amber-400"
|
||||
class="icon-[comfy--credits] size-4 shrink-0 bg-credit"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Button
|
||||
:size
|
||||
:disabled="disabled"
|
||||
:variant="buttonVariant === 'gradient' ? 'gradient' : 'primary'"
|
||||
:variant="buttonVariant === 'subscribe' ? 'subscribe' : 'primary'"
|
||||
:class="cn('font-bold', fluid && 'w-full')"
|
||||
@click="handleSubscribe"
|
||||
>
|
||||
@@ -28,7 +28,7 @@ const {
|
||||
} = defineProps<{
|
||||
label?: string
|
||||
size?: 'sm' | 'lg'
|
||||
buttonVariant?: 'default' | 'gradient'
|
||||
buttonVariant?: 'default' | 'subscribe'
|
||||
fluid?: boolean
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
showDelay: 600
|
||||
}"
|
||||
class="subscribe-to-run-button h-8 gap-1.5 rounded-lg px-4 whitespace-nowrap"
|
||||
variant="gradient"
|
||||
variant="subscribe"
|
||||
size="unset"
|
||||
data-testid="subscribe-to-run-button"
|
||||
@click="handleSubscribeToRun"
|
||||
|
||||
@@ -520,31 +520,6 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
type: 'text',
|
||||
defaultValue: ''
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Image.PreviewCompression',
|
||||
category: ['Comfy', 'Preview', 'PreviewCompression'],
|
||||
name: 'Load large images as compressed previews',
|
||||
tooltip:
|
||||
'When enabled, image editors (currently the mask editor) load images as compressed, downscaled previews. This greatly improves performance with very large images; edits are applied to the original at full resolution on save. Downscaling requires the assets system on the server (--enable-assets); without it images are only recompressed at full resolution.',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
versionAdded: '1.48.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Image.PreviewMaxSize',
|
||||
category: ['Comfy', 'Preview', 'PreviewMaxSize'],
|
||||
name: 'Compressed preview maximum size',
|
||||
tooltip:
|
||||
'Maximum size in pixels for the longest edge of a compressed preview. Images whose longest edge exceeds this are downscaled proportionally until it fits; the aspect ratio is preserved and images are never upscaled.',
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 512,
|
||||
max: 8192,
|
||||
step: 256
|
||||
},
|
||||
defaultValue: 4096,
|
||||
versionAdded: '1.48.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.DisableSliders',
|
||||
category: ['LiteGraph', 'Node Widget', 'DisableSliders'],
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<!-- Credits Section -->
|
||||
|
||||
<div class="flex items-center gap-2 px-4 py-2">
|
||||
<i class="icon-[lucide--component] text-sm text-amber-400" />
|
||||
<i class="icon-[lucide--component] text-sm text-credit" />
|
||||
<Skeleton
|
||||
v-if="isLoadingBalance"
|
||||
width="4rem"
|
||||
@@ -82,7 +82,7 @@
|
||||
<!-- Upgrade to add credits (free tier) -->
|
||||
<Button
|
||||
v-if="isActiveSubscription && permissions.canTopUp && isFreeTier"
|
||||
variant="gradient"
|
||||
variant="subscribe"
|
||||
size="sm"
|
||||
data-testid="upgrade-to-add-credits-button"
|
||||
@click="handleUpgradeToAddCredits"
|
||||
@@ -110,7 +110,7 @@
|
||||
: $t('workspaceSwitcher.subscribe')
|
||||
"
|
||||
size="sm"
|
||||
button-variant="gradient"
|
||||
button-variant="subscribe"
|
||||
/>
|
||||
<Button
|
||||
v-if="
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
{{ t('subscription.monthlyCreditsPerMemberLabel') }}
|
||||
</span>
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<i class="icon-[lucide--component] text-sm text-amber-400" />
|
||||
<i class="icon-[lucide--component] text-sm text-credit" />
|
||||
<span
|
||||
class="font-inter text-sm/normal font-bold text-base-foreground"
|
||||
>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
{{ $t(creditsRefillLabelKey) }}
|
||||
</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<i class="icon-[comfy--credits] size-4 shrink-0 bg-amber-400" />
|
||||
<i class="icon-[comfy--credits] size-4 shrink-0 bg-credit" />
|
||||
<span class="font-bold text-base-foreground">
|
||||
{{ refillCredits }}
|
||||
</span>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 text-sm text-muted-foreground">
|
||||
<i class="icon-[comfy--credits] size-4 shrink-0 bg-amber-400" />
|
||||
<i class="icon-[comfy--credits] size-4 shrink-0 bg-credit" />
|
||||
<span>{{ displayCredits }} {{ $t('subscription.perMonth') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-base-foreground">{{ refillLabel }}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<i class="icon-[comfy--credits] size-4 shrink-0 bg-amber-400" />
|
||||
<i class="icon-[comfy--credits] size-4 shrink-0 bg-credit" />
|
||||
<span class="font-bold text-base-foreground">{{
|
||||
refillCredits
|
||||
}}</span>
|
||||
@@ -89,7 +89,7 @@
|
||||
{{ $t('subscription.preview.creditsRefillMonthlyTo') }}
|
||||
</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<i class="icon-[comfy--credits] size-4 shrink-0 bg-amber-400" />
|
||||
<i class="icon-[comfy--credits] size-4 shrink-0 bg-credit" />
|
||||
<span class="font-bold text-base-foreground">{{
|
||||
monthlyRefillCredits
|
||||
}}</span>
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
<div class="mt-auto flex flex-col gap-1">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<i
|
||||
class="icon-[comfy--credits] size-4 shrink-0 bg-amber-400"
|
||||
class="icon-[comfy--credits] size-4 shrink-0 bg-credit"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span
|
||||
@@ -244,7 +244,7 @@
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<i
|
||||
class="icon-[comfy--credits] size-4 shrink-0 bg-amber-400"
|
||||
class="icon-[comfy--credits] size-4 shrink-0 bg-credit"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span
|
||||
|
||||
@@ -7,7 +7,7 @@ defineProps<{ title: string; price: string }>()
|
||||
<span
|
||||
class="mt-2 flex h-5 max-w-max items-center rounded-full bg-component-node-widget-background p-2 py-3"
|
||||
>
|
||||
<i class="mr-1 icon-[lucide--component] h-4 bg-amber-400" />
|
||||
<i class="mr-1 icon-[lucide--component] h-4 bg-credit" />
|
||||
{{ price }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@ const { t } = useI18n()
|
||||
<Popover v-if="mobile && creditsBadges.length" side="top">
|
||||
<template #button>
|
||||
<Button class="mr-2 size-10">
|
||||
<i class="icon-[comfy--credits] bg-amber-400" />
|
||||
<i class="icon-[comfy--credits] bg-credit" />
|
||||
</Button>
|
||||
</template>
|
||||
<section
|
||||
@@ -51,7 +51,7 @@ const { t } = useI18n()
|
||||
<div class="mb-1 w-full border-b border-border-subtle" />
|
||||
<CollapsibleTrigger as-child>
|
||||
<Button variant="textonly" class="w-full text-sm">
|
||||
<i class="icon-[comfy--credits] size-4 bg-amber-400" />
|
||||
<i class="icon-[comfy--credits] size-4 bg-credit" />
|
||||
{{ t('linearMode.hasCreditCost') }}
|
||||
<i v-if="open" class="ml-auto icon-[lucide--chevron-up]" />
|
||||
<i v-else class="ml-auto icon-[lucide--chevron-down]" />
|
||||
|
||||
@@ -438,8 +438,6 @@ const zSettings = z.object({
|
||||
'Comfy-Desktop.UV.TorchInstallMirror': z.string(),
|
||||
'Comfy.MaskEditor.BrushAdjustmentSpeed': z.number(),
|
||||
'Comfy.MaskEditor.UseDominantAxis': z.boolean(),
|
||||
'Comfy.Image.PreviewCompression': z.boolean(),
|
||||
'Comfy.Image.PreviewMaxSize': z.number(),
|
||||
'Comfy.Load3D.ShowGrid': z.boolean(),
|
||||
'Comfy.Load3D.BackgroundColor': z.string(),
|
||||
'Comfy.Load3D.LightIntensity': z.number(),
|
||||
|
||||
Reference in New Issue
Block a user