mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-17 01:07:56 +00:00
Compare commits
8 Commits
fix/code-q
...
fix/clean-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ddc8739bb | ||
|
|
16119dfcd2 | ||
|
|
a6f1b1cf90 | ||
|
|
c95d32249b | ||
|
|
740df0470e | ||
|
|
dccf68ddb7 | ||
|
|
117448fba4 | ||
|
|
da77227cf2 |
@@ -40,6 +40,7 @@ import InputText from 'primevue/inputtext'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { appendCloudResParam } from '@/platform/distribution/cloudPreviewUtil'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
@@ -88,6 +89,7 @@ const handleFileUpload = async (event: Event) => {
|
||||
type: 'input',
|
||||
subfolder: 'backgrounds'
|
||||
})
|
||||
appendCloudResParam(params, file.name)
|
||||
modelValue.value = `/api/view?${params.toString()}`
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -28,8 +28,9 @@
|
||||
/>
|
||||
<div
|
||||
v-show="cursorVisible"
|
||||
class="pointer-events-none absolute left-0 top-0 rounded-full border border-black/60 shadow-[0_0_0_1px_rgba(255,255,255,0.8)]"
|
||||
:style="cursorStyle"
|
||||
ref="cursorEl"
|
||||
class="pointer-events-none absolute left-0 top-0 rounded-full border border-black/60 shadow-[0_0_0_1px_rgba(255,255,255,0.8)] will-change-transform"
|
||||
:style="cursorSizeStyle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,7 +142,7 @@
|
||||
max="100"
|
||||
step="1"
|
||||
class="w-7 appearance-none border-0 bg-transparent text-right text-xs text-node-text-muted outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none [-moz-appearance:textfield]"
|
||||
@click.prevent
|
||||
@click.stop
|
||||
@change="
|
||||
(e) => {
|
||||
const val = Math.min(
|
||||
@@ -281,6 +282,7 @@ const { nodeId } = defineProps<{
|
||||
const modelValue = defineModel<string>({ default: '' })
|
||||
|
||||
const canvasEl = useTemplateRef<HTMLCanvasElement>('canvasEl')
|
||||
const cursorEl = useTemplateRef<HTMLElement>('cursorEl')
|
||||
const controlsEl = useTemplateRef<HTMLDivElement>('controlsEl')
|
||||
const { width: controlsWidth } = useElementSize(controlsEl)
|
||||
const compact = computed(
|
||||
@@ -296,8 +298,6 @@ const {
|
||||
backgroundColor,
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
cursorX,
|
||||
cursorY,
|
||||
cursorVisible,
|
||||
displayBrushSize,
|
||||
inputImageUrl,
|
||||
@@ -309,7 +309,7 @@ const {
|
||||
handlePointerLeave,
|
||||
handleInputImageLoad,
|
||||
handleClear
|
||||
} = usePainter(nodeId, { canvasEl, modelValue })
|
||||
} = usePainter(nodeId, { canvasEl, cursorEl, modelValue })
|
||||
|
||||
const canvasContainerStyle = computed(() => ({
|
||||
aspectRatio: `${canvasWidth.value} / ${canvasHeight.value}`,
|
||||
@@ -318,16 +318,10 @@ const canvasContainerStyle = computed(() => ({
|
||||
: backgroundColor.value
|
||||
}))
|
||||
|
||||
const cursorStyle = computed(() => {
|
||||
const size = displayBrushSize.value
|
||||
const x = cursorX.value - size / 2
|
||||
const y = cursorY.value - size / 2
|
||||
return {
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
transform: `translate(${x}px, ${y}px)`
|
||||
}
|
||||
})
|
||||
const cursorSizeStyle = computed(() => ({
|
||||
width: `${displayBrushSize.value}px`,
|
||||
height: `${displayBrushSize.value}px`
|
||||
}))
|
||||
|
||||
const brushOpacityPercent = computed({
|
||||
get: () => Math.round(brushOpacity.value * 100),
|
||||
|
||||
@@ -27,11 +27,12 @@ export const PAINTER_TOOLS: Record<string, PainterTool> = {
|
||||
|
||||
interface UsePainterOptions {
|
||||
canvasEl: Ref<HTMLCanvasElement | null>
|
||||
cursorEl: Ref<HTMLElement | null>
|
||||
modelValue: Ref<string>
|
||||
}
|
||||
|
||||
export function usePainter(nodeId: string, options: UsePainterOptions) {
|
||||
const { canvasEl, modelValue } = options
|
||||
const { canvasEl, cursorEl, modelValue } = options
|
||||
const { t } = useI18n()
|
||||
const nodeOutputStore = useNodeOutputStore()
|
||||
const toastStore = useToastStore()
|
||||
@@ -41,8 +42,6 @@ export function usePainter(nodeId: string, options: UsePainterOptions) {
|
||||
const canvasWidth = ref(512)
|
||||
const canvasHeight = ref(512)
|
||||
|
||||
const cursorX = ref(0)
|
||||
const cursorY = ref(0)
|
||||
const cursorVisible = ref(false)
|
||||
|
||||
const inputImageUrl = ref<string | null>(null)
|
||||
@@ -518,8 +517,10 @@ export function usePainter(nodeId: string, options: UsePainterOptions) {
|
||||
}
|
||||
|
||||
function updateCursorPos(e: PointerEvent) {
|
||||
cursorX.value = e.offsetX
|
||||
cursorY.value = e.offsetY
|
||||
const el = cursorEl.value
|
||||
if (!el) return
|
||||
const size = displayBrushSize.value
|
||||
el.style.transform = `translate(${e.offsetX - size / 2}px, ${e.offsetY - size / 2}px)`
|
||||
}
|
||||
|
||||
function handlePointerDown(e: PointerEvent) {
|
||||
@@ -760,8 +761,6 @@ export function usePainter(nodeId: string, options: UsePainterOptions) {
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
|
||||
cursorX,
|
||||
cursorY,
|
||||
cursorVisible,
|
||||
displayBrushSize,
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LLink } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
|
||||
function applyToGraph(this: LGraphNode, extraLinks: LLink[] = []) {
|
||||
if (!this.outputs[0].links?.length || !this.graph) return
|
||||
@@ -74,17 +75,25 @@ function onCustomComboCreated(this: LGraphNode) {
|
||||
function addOption(node: LGraphNode) {
|
||||
if (!node.widgets) return
|
||||
const newCount = node.widgets.length - 1
|
||||
node.addWidget('string', `option${newCount}`, '', () => {})
|
||||
const widget = node.widgets.at(-1)
|
||||
const widgetName = `option${newCount}`
|
||||
const widget = node.addWidget('string', widgetName, '', () => {})
|
||||
if (!widget) return
|
||||
|
||||
let value = ''
|
||||
Object.defineProperty(widget, 'value', {
|
||||
get() {
|
||||
return value
|
||||
return useWidgetValueStore().getWidget(
|
||||
app.rootGraph.id,
|
||||
node.id,
|
||||
widgetName
|
||||
)?.value
|
||||
},
|
||||
set(v) {
|
||||
value = v
|
||||
set(v: string) {
|
||||
const state = useWidgetValueStore().getWidget(
|
||||
app.rootGraph.id,
|
||||
node.id,
|
||||
widgetName
|
||||
)
|
||||
if (state) state.value = v
|
||||
updateCombo()
|
||||
if (!node.widgets) return
|
||||
const lastWidget = node.widgets.at(-1)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { NodeOutputWith } from '@/schemas/apiSchema'
|
||||
import { appendCloudResParam } from '@/platform/distribution/cloudPreviewUtil'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
@@ -26,8 +27,11 @@ useExtensionService().registerExtension({
|
||||
const { a_images: aImages, b_images: bImages } = output
|
||||
const rand = app.getRandParam()
|
||||
|
||||
const toUrl = (params: Record<string, string>) =>
|
||||
api.apiURL(`/view?${new URLSearchParams(params)}${rand}`)
|
||||
const toUrl = (record: Record<string, string>) => {
|
||||
const params = new URLSearchParams(record)
|
||||
appendCloudResParam(params)
|
||||
return api.apiURL(`/view?${params}${rand}`)
|
||||
}
|
||||
|
||||
const beforeImages =
|
||||
aImages && aImages.length > 0 ? aImages.map(toUrl) : []
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import type { OutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
|
||||
import type { AssetContext } from '@/platform/assets/schemas/mediaAssetSchema'
|
||||
import { appendCloudResParam } from '@/platform/distribution/cloudPreviewUtil'
|
||||
import { api } from '@/scripts/api'
|
||||
import type { ResultItemImpl, TaskItemImpl } from '@/stores/queueStore'
|
||||
|
||||
@@ -43,7 +44,7 @@ export function mapTaskOutputToAssetItem(
|
||||
? new Date(taskItem.executionStartTimestamp).toISOString()
|
||||
: new Date().toISOString(),
|
||||
tags: ['output'],
|
||||
preview_url: output.url,
|
||||
preview_url: output.previewUrl,
|
||||
user_metadata: metadata
|
||||
}
|
||||
}
|
||||
@@ -60,14 +61,15 @@ export function mapInputFileToAssetItem(
|
||||
index: number,
|
||||
directory: 'input' | 'output' = 'input'
|
||||
): AssetItem {
|
||||
const params = new URLSearchParams({ filename, type: directory })
|
||||
appendCloudResParam(params, filename)
|
||||
|
||||
return {
|
||||
id: `${directory}-${index}-${filename}`,
|
||||
name: filename,
|
||||
size: 0,
|
||||
created_at: new Date().toISOString(),
|
||||
tags: [directory],
|
||||
preview_url: api.apiURL(
|
||||
`/view?filename=${encodeURIComponent(filename)}&type=${directory}`
|
||||
)
|
||||
preview_url: api.apiURL(`/view?${params}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,16 @@ type OutputOverrides = Partial<{
|
||||
}>
|
||||
|
||||
function createOutput(overrides: OutputOverrides = {}): ResultItemImpl {
|
||||
return {
|
||||
const merged = {
|
||||
filename: 'file.png',
|
||||
subfolder: 'sub',
|
||||
nodeId: '1',
|
||||
url: 'https://example.com/file.png',
|
||||
...overrides
|
||||
}
|
||||
return {
|
||||
...merged,
|
||||
previewUrl: merged.url
|
||||
} as ResultItemImpl
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ function mapOutputsToAssetItems({
|
||||
size: 0,
|
||||
created_at: createdAtValue,
|
||||
tags: ['output'],
|
||||
preview_url: output.url,
|
||||
preview_url: output.previewUrl,
|
||||
user_metadata: {
|
||||
jobId,
|
||||
nodeId: output.nodeId,
|
||||
|
||||
56
src/platform/distribution/cloudPreviewUtil.test.ts
Normal file
56
src/platform/distribution/cloudPreviewUtil.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { appendCloudResParam } from './cloudPreviewUtil'
|
||||
|
||||
const mockIsCloud = vi.hoisted(() => ({ value: false }))
|
||||
|
||||
vi.mock('./types', () => ({
|
||||
get isCloud() {
|
||||
return mockIsCloud.value
|
||||
}
|
||||
}))
|
||||
|
||||
function buildParams(filename?: string): URLSearchParams {
|
||||
const params = new URLSearchParams()
|
||||
appendCloudResParam(params, filename)
|
||||
return params
|
||||
}
|
||||
|
||||
describe('appendCloudResParam', () => {
|
||||
it('does not set res in non-cloud mode', () => {
|
||||
mockIsCloud.value = false
|
||||
expect(buildParams('test.png').has('res')).toBe(false)
|
||||
})
|
||||
|
||||
it.for(['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'tiff', 'tif'])(
|
||||
'sets res=512 for .%s in cloud mode',
|
||||
(ext) => {
|
||||
mockIsCloud.value = true
|
||||
const params = buildParams(`file.${ext}`)
|
||||
expect(params.get('res')).toBe('512')
|
||||
}
|
||||
)
|
||||
|
||||
it.for([
|
||||
'video.mp4',
|
||||
'video.webm',
|
||||
'audio.mp3',
|
||||
'audio.wav',
|
||||
'model.glb',
|
||||
'icon.svg'
|
||||
])('does not set res for %s in cloud mode', (name) => {
|
||||
mockIsCloud.value = true
|
||||
expect(buildParams(name).has('res')).toBe(false)
|
||||
})
|
||||
|
||||
it('sets res=512 when no filename provided in cloud mode', () => {
|
||||
mockIsCloud.value = true
|
||||
expect(buildParams().get('res')).toBe('512')
|
||||
expect(buildParams(undefined).get('res')).toBe('512')
|
||||
})
|
||||
|
||||
it('does not set res when no filename provided in non-cloud mode', () => {
|
||||
mockIsCloud.value = false
|
||||
expect(buildParams().has('res')).toBe(false)
|
||||
})
|
||||
})
|
||||
19
src/platform/distribution/cloudPreviewUtil.ts
Normal file
19
src/platform/distribution/cloudPreviewUtil.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { getMediaTypeFromFilename } from '@/utils/formatUtil'
|
||||
|
||||
import { isCloud } from './types'
|
||||
|
||||
/**
|
||||
* Appends `res=512` to the given URLSearchParams when in cloud mode
|
||||
* and the file is an image (or filename is unknown).
|
||||
*
|
||||
* The cloud backend resizes images server-side to prevent
|
||||
* the frontend from loading very large originals for previews.
|
||||
*/
|
||||
export function appendCloudResParam(
|
||||
params: URLSearchParams,
|
||||
filename?: string
|
||||
): void {
|
||||
if (!isCloud) return
|
||||
if (filename && getMediaTypeFromFilename(filename) !== 'image') return
|
||||
params.set('res', '512')
|
||||
}
|
||||
130
src/platform/keybindings/keybindingService.test.ts
Normal file
130
src/platform/keybindings/keybindingService.test.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useKeybindingService } from '@/platform/keybindings/keybindingService'
|
||||
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
|
||||
import type { Keybinding } from '@/platform/keybindings/types'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const mockSettingState = vi.hoisted(() => ({
|
||||
newBindings: [] as Keybinding[],
|
||||
unsetBindings: [] as Keybinding[],
|
||||
setMany: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: vi.fn(() => ({
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === 'Comfy.Keybinding.NewBindings')
|
||||
return mockSettingState.newBindings
|
||||
if (key === 'Comfy.Keybinding.UnsetBindings')
|
||||
return mockSettingState.unsetBindings
|
||||
return []
|
||||
}),
|
||||
setMany: mockSettingState.setMany
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: { canvas: null }
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/dialogStore', () => ({
|
||||
useDialogStore: vi.fn(() => ({ dialogStack: [] }))
|
||||
}))
|
||||
|
||||
describe('keybindingService - orphaned keybinding cleanup', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setActivePinia(createPinia())
|
||||
mockSettingState.newBindings = []
|
||||
mockSettingState.unsetBindings = []
|
||||
})
|
||||
|
||||
function registerCommand(commandId: string) {
|
||||
useCommandStore().registerCommand({ id: commandId, function: vi.fn() })
|
||||
}
|
||||
|
||||
it('should skip orphaned new bindings referencing unregistered commands', () => {
|
||||
registerCommand('Registered.Command')
|
||||
|
||||
mockSettingState.newBindings = [
|
||||
{ commandId: 'Registered.Command', combo: { key: 'A', ctrl: true } },
|
||||
{
|
||||
commandId: 'Removed.Extension.Command',
|
||||
combo: { key: 'B', alt: true }
|
||||
}
|
||||
]
|
||||
|
||||
const service = useKeybindingService()
|
||||
service.registerCoreKeybindings()
|
||||
service.registerUserKeybindings()
|
||||
|
||||
const keybindingStore = useKeybindingStore()
|
||||
expect(
|
||||
keybindingStore.getKeybindingsByCommandId('Registered.Command')
|
||||
).toHaveLength(1)
|
||||
expect(
|
||||
keybindingStore.getKeybindingsByCommandId('Removed.Extension.Command')
|
||||
).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should skip orphaned unset bindings referencing unregistered commands', () => {
|
||||
registerCommand('Registered.Command')
|
||||
|
||||
const registeredBinding: Keybinding = {
|
||||
commandId: 'Registered.Command',
|
||||
combo: { key: 'A', ctrl: true }
|
||||
}
|
||||
|
||||
mockSettingState.unsetBindings = [
|
||||
registeredBinding,
|
||||
{
|
||||
commandId: 'Removed.Extension.Command',
|
||||
combo: { key: 'B', alt: true }
|
||||
}
|
||||
]
|
||||
|
||||
const keybindingStore = useKeybindingStore()
|
||||
const unsetSpy = vi.spyOn(keybindingStore, 'unsetKeybinding')
|
||||
|
||||
const service = useKeybindingService()
|
||||
service.registerCoreKeybindings()
|
||||
service.registerUserKeybindings()
|
||||
|
||||
expect(unsetSpy).toHaveBeenCalledTimes(1)
|
||||
expect(unsetSpy.mock.calls[0][0].commandId).toBe('Registered.Command')
|
||||
})
|
||||
|
||||
it('should persist cleanup when orphaned bindings are found', () => {
|
||||
registerCommand('Registered.Command')
|
||||
|
||||
mockSettingState.newBindings = [
|
||||
{ commandId: 'Registered.Command', combo: { key: 'A', ctrl: true } },
|
||||
{
|
||||
commandId: 'Removed.Extension.Command',
|
||||
combo: { key: 'B', alt: true }
|
||||
}
|
||||
]
|
||||
|
||||
const service = useKeybindingService()
|
||||
service.registerCoreKeybindings()
|
||||
service.registerUserKeybindings()
|
||||
|
||||
expect(mockSettingState.setMany).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not persist when no orphaned bindings exist', () => {
|
||||
registerCommand('Registered.Command')
|
||||
|
||||
mockSettingState.newBindings = [
|
||||
{ commandId: 'Registered.Command', combo: { key: 'A', ctrl: true } }
|
||||
]
|
||||
|
||||
const service = useKeybindingService()
|
||||
service.registerCoreKeybindings()
|
||||
service.registerUserKeybindings()
|
||||
|
||||
expect(mockSettingState.setMany).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -121,11 +121,20 @@ export function useKeybindingService() {
|
||||
|
||||
function registerUserKeybindings() {
|
||||
const unsetBindings = settingStore.get('Comfy.Keybinding.UnsetBindings')
|
||||
for (const keybinding of unsetBindings) {
|
||||
const registeredUnsetBindings = unsetBindings.filter((kb) =>
|
||||
commandStore.isRegistered(kb.commandId)
|
||||
)
|
||||
|
||||
for (const keybinding of registeredUnsetBindings) {
|
||||
keybindingStore.unsetKeybinding(new KeybindingImpl(keybinding))
|
||||
}
|
||||
|
||||
const newBindings = settingStore.get('Comfy.Keybinding.NewBindings')
|
||||
for (const keybinding of newBindings) {
|
||||
const registeredNewBindings = newBindings.filter((kb) =>
|
||||
commandStore.isRegistered(kb.commandId)
|
||||
)
|
||||
|
||||
for (const keybinding of registeredNewBindings) {
|
||||
if (
|
||||
isCloud &&
|
||||
keybinding.commandId === 'Workspace.ToggleBottomPanelTab.logs-terminal'
|
||||
@@ -134,6 +143,13 @@ export function useKeybindingService() {
|
||||
}
|
||||
keybindingStore.addUserKeybinding(new KeybindingImpl(keybinding))
|
||||
}
|
||||
|
||||
const hadOrphans =
|
||||
registeredUnsetBindings.length < unsetBindings.length ||
|
||||
registeredNewBindings.length < newBindings.length
|
||||
if (hadOrphans) {
|
||||
void persistUserKeybindings()
|
||||
}
|
||||
}
|
||||
|
||||
async function persistUserKeybindings() {
|
||||
|
||||
@@ -10,6 +10,7 @@ import Button from '@/components/ui/button/Button.vue'
|
||||
import { extractVueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useBillingContext } from '@/composables/billing/useBillingContext'
|
||||
import { appendCloudResParam } from '@/platform/distribution/cloudPreviewUtil'
|
||||
import SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
@@ -88,13 +89,16 @@ function getDropIndicator(node: LGraphNode) {
|
||||
const filename = node.widgets?.[0]?.value
|
||||
const resultItem = { type: 'input', filename: `${filename}` }
|
||||
|
||||
const buildImageUrl = () => {
|
||||
if (!filename) return undefined
|
||||
const params = new URLSearchParams(resultItem)
|
||||
appendCloudResParam(params, String(filename))
|
||||
return api.apiURL(`/view?${params}${app.getPreviewFormatParam()}`)
|
||||
}
|
||||
|
||||
return {
|
||||
iconClass: 'icon-[lucide--image]',
|
||||
imageUrl: filename
|
||||
? api.apiURL(
|
||||
`/view?${new URLSearchParams(resultItem)}${app.getPreviewFormatParam()}`
|
||||
)
|
||||
: undefined,
|
||||
imageUrl: buildImageUrl(),
|
||||
label: t('linearMode.dragAndDropImage'),
|
||||
onClick: () => node.widgets?.[1]?.callback?.(undefined)
|
||||
}
|
||||
|
||||
@@ -243,7 +243,15 @@ const handleDownload = () => {
|
||||
|
||||
const handleRemove = () => {
|
||||
if (!props.nodeId) return
|
||||
const node = app.rootGraph?.getNodeById(Number(props.nodeId))
|
||||
nodeOutputStore.removeNodeOutputs(props.nodeId)
|
||||
if (node) {
|
||||
node.imgs = undefined
|
||||
const imageWidget = node.widgets?.find((w) => w.name === 'image')
|
||||
if (imageWidget) {
|
||||
imageWidget.value = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const setCurrentIndex = (index: number) => {
|
||||
|
||||
@@ -93,6 +93,11 @@ function useNodeEventHandlersIndividual() {
|
||||
|
||||
// Update the node title in LiteGraph for persistence
|
||||
node.title = newTitle
|
||||
|
||||
// If this is a subgraph node, sync the subgraph name for breadcrumb reactivity
|
||||
if (node.isSubgraphNode?.()) {
|
||||
node.subgraph.name = newTitle
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div class="relative" @pointerdown.stop>
|
||||
<div class="mb-4">
|
||||
<Button
|
||||
class="text-base-foreground w-full border-0 bg-secondary-background hover:bg-secondary-background-hover"
|
||||
|
||||
@@ -4,6 +4,7 @@ import { computed, provide, ref, toRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
|
||||
import { appendCloudResParam } from '@/platform/distribution/cloudPreviewUtil'
|
||||
import { SUPPORTED_EXTENSIONS_ACCEPT } from '@/extensions/core/load3d/constants'
|
||||
import { useAssetFilterOptions } from '@/platform/assets/composables/useAssetFilterOptions'
|
||||
import {
|
||||
@@ -460,7 +461,9 @@ function getMediaUrl(
|
||||
type: 'input' | 'output' = 'input'
|
||||
): string {
|
||||
if (!['image', 'video'].includes(props.assetKind ?? '')) return ''
|
||||
return `/api/view?filename=${encodeURIComponent(filename)}&type=${type}`
|
||||
const params = new URLSearchParams({ filename, type })
|
||||
appendCloudResParam(params, filename)
|
||||
return `/api/view?${params}`
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div class="relative" @pointerdown.stop>
|
||||
<div
|
||||
v-if="!hideWhenEmpty || modelValue"
|
||||
class="bg-component-node-widget-background box-border flex gap-4 items-center justify-start relative rounded-lg w-full h-16 px-4 py-0"
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
ResultItem,
|
||||
ResultItemType
|
||||
} from '@/schemas/apiSchema'
|
||||
import { appendCloudResParam } from '@/platform/distribution/cloudPreviewUtil'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { NodeLocatorId } from '@/types/nodeIdentification'
|
||||
@@ -118,10 +119,13 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
|
||||
const rand = app.getRandParam()
|
||||
const previewParam = getPreviewParam(node, outputs)
|
||||
const isImage = isImageOutputs(node, outputs)
|
||||
const firstFilename = outputs.images[0]?.filename
|
||||
|
||||
return outputs.images.map((image) => {
|
||||
const imgUrlPart = new URLSearchParams(image)
|
||||
return api.apiURL(`/view?${imgUrlPart}${previewParam}${rand}`)
|
||||
const params = new URLSearchParams(image)
|
||||
if (isImage) appendCloudResParam(params, firstFilename)
|
||||
return api.apiURL(`/view?${params}${previewParam}${rand}`)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
StatusWsMessageStatus,
|
||||
TaskOutput
|
||||
} from '@/schemas/apiSchema'
|
||||
import { appendCloudResParam } from '@/platform/distribution/cloudPreviewUtil'
|
||||
import { api } from '@/scripts/api'
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
@@ -91,6 +92,13 @@ export class ResultItemImpl {
|
||||
return api.apiURL('/view?' + this.urlParams)
|
||||
}
|
||||
|
||||
get previewUrl(): string {
|
||||
if (!this.isImage) return this.url
|
||||
const params = new URLSearchParams(this.urlParams)
|
||||
appendCloudResParam(params, this.filename)
|
||||
return api.apiURL('/view?' + params)
|
||||
}
|
||||
|
||||
get urlWithTimestamp(): string {
|
||||
return `${this.url}&t=${+new Date()}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user