mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-09 23:20:04 +00:00
feat: audio drag-drop and paste support (#9152)
This commit is contained in:
@@ -100,21 +100,24 @@ describe('ComfyApp', () => {
|
||||
expect(mockNode2.connect).toHaveBeenCalledWith(0, mockBatchNode, 1)
|
||||
})
|
||||
|
||||
it('should not proceed if batch node creation fails', async () => {
|
||||
it('should select single image node without batch node', async () => {
|
||||
const mockNode1 = createMockNode({ id: 1 })
|
||||
vi.mocked(pasteImageNodes).mockResolvedValue([mockNode1])
|
||||
vi.mocked(createNode).mockResolvedValue(null)
|
||||
|
||||
const file = createTestFile('test.png', 'image/png')
|
||||
|
||||
await app.handleFileList([file])
|
||||
|
||||
expect(mockCanvas.selectItems).not.toHaveBeenCalled()
|
||||
expect(createNode).not.toHaveBeenCalled()
|
||||
expect(mockCanvas.selectItems).toHaveBeenCalledWith([mockNode1])
|
||||
expect(mockNode1.connect).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle empty file list', async () => {
|
||||
await expect(app.handleFileList([])).rejects.toThrow()
|
||||
await app.handleFileList([])
|
||||
|
||||
expect(pasteImageNodes).not.toHaveBeenCalled()
|
||||
expect(createNode).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not process unsupported file types', async () => {
|
||||
|
||||
@@ -111,9 +111,18 @@ import { ComfyAppMenu } from './ui/menu/index'
|
||||
import { clone } from './utils'
|
||||
import { type ComfyWidgetConstructor } from './widgets'
|
||||
import { ensureCorrectLayoutScale } from '@/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale'
|
||||
import { extractFilesFromDragEvent, hasImageType } from '@/utils/eventUtils'
|
||||
import {
|
||||
extractFilesFromDragEvent,
|
||||
hasAudioType,
|
||||
hasImageType
|
||||
} from '@/utils/eventUtils'
|
||||
import { getWorkflowDataFromFile } from '@/scripts/metadata/parser'
|
||||
import { pasteImageNode, pasteImageNodes } from '@/composables/usePaste'
|
||||
import {
|
||||
pasteAudioNode,
|
||||
pasteAudioNodes,
|
||||
pasteImageNode,
|
||||
pasteImageNodes
|
||||
} from '@/composables/usePaste'
|
||||
|
||||
export const ANIM_PREVIEW_WIDGET = '$$comfy_animation_preview'
|
||||
|
||||
@@ -560,8 +569,24 @@ export class ComfyApp {
|
||||
const workspace = useWorkspaceStore()
|
||||
try {
|
||||
workspace.spinner = true
|
||||
if (files.length > 1 && files.every(hasImageType)) {
|
||||
await this.handleFileList(files)
|
||||
const imageFiles = files.filter(hasImageType)
|
||||
const audioFiles = files.filter(hasAudioType)
|
||||
const totalMedia = imageFiles.length + audioFiles.length
|
||||
const hasMultipleMedia = totalMedia > 1
|
||||
|
||||
if (hasMultipleMedia) {
|
||||
if (imageFiles.length > 0) {
|
||||
await this.handleFileList(imageFiles)
|
||||
}
|
||||
if (audioFiles.length > 0) {
|
||||
await this.handleAudioFileList(audioFiles)
|
||||
}
|
||||
const handled = new Set([...imageFiles, ...audioFiles])
|
||||
for (const file of files.filter((f) => !handled.has(f))) {
|
||||
await this.handleFile(file, 'file_drop', {
|
||||
deferWarnings: true
|
||||
})
|
||||
}
|
||||
} else {
|
||||
for (const file of files) {
|
||||
await this.handleFile(file, 'file_drop', {
|
||||
@@ -1562,6 +1587,12 @@ export class ComfyApp {
|
||||
const imageNode = await createNode(this.canvas, 'LoadImage')
|
||||
await pasteImageNode(this.canvas, transfer.items, imageNode)
|
||||
return
|
||||
} else if (file.type.startsWith('audio')) {
|
||||
const transfer = new DataTransfer()
|
||||
transfer.items.add(file)
|
||||
const audioNode = await createNode(this.canvas, 'LoadAudio')
|
||||
await pasteAudioNode(this.canvas, transfer.items, audioNode)
|
||||
return
|
||||
}
|
||||
|
||||
this.showErrorOnFileLoad(file)
|
||||
@@ -1643,25 +1674,55 @@ export class ComfyApp {
|
||||
* @param {FileList} fileList
|
||||
*/
|
||||
async handleFileList(fileList: File[]) {
|
||||
if (fileList[0].type.startsWith('image')) {
|
||||
const imageNodes = await pasteImageNodes(this.canvas, fileList)
|
||||
if (fileList.length === 0) return
|
||||
if (!fileList[0].type.startsWith('image')) return
|
||||
|
||||
const imageNodes = await pasteImageNodes(this.canvas, fileList)
|
||||
if (imageNodes.length === 0) return
|
||||
|
||||
if (imageNodes.length > 1) {
|
||||
const batchImagesNode = await createNode(this.canvas, 'BatchImagesNode')
|
||||
if (!batchImagesNode) return
|
||||
|
||||
this.positionBatchNodes(imageNodes, batchImagesNode)
|
||||
this.canvas.selectItems([...imageNodes, batchImagesNode])
|
||||
|
||||
Array.from(imageNodes).forEach((imageNode, index) => {
|
||||
imageNodes.forEach((imageNode, index) => {
|
||||
imageNode.connect(0, batchImagesNode, index)
|
||||
})
|
||||
} else {
|
||||
this.canvas.selectItems(imageNodes)
|
||||
}
|
||||
}
|
||||
|
||||
async handleAudioFileList(fileList: File[]) {
|
||||
const audioNodes = await pasteAudioNodes(this.canvas, fileList)
|
||||
if (audioNodes.length === 0) return
|
||||
|
||||
this.positionNodes(audioNodes)
|
||||
this.canvas.selectItems(audioNodes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Positions batched nodes in drag and drop
|
||||
* @param nodes
|
||||
* @param batchNode
|
||||
*/
|
||||
positionNodes(nodes: LGraphNode[]): void {
|
||||
if (nodes.length <= 1) return
|
||||
|
||||
const [x, y] = nodes[0].getBounding()
|
||||
const nodeHeight = 150
|
||||
|
||||
nodes.forEach((node, index) => {
|
||||
if (index > 0) {
|
||||
node.pos = [x, y + nodeHeight * index + 25 * (index + 1)]
|
||||
}
|
||||
})
|
||||
|
||||
this.canvas.graph?.change()
|
||||
}
|
||||
|
||||
positionBatchNodes(nodes: LGraphNode[], batchNode: LGraphNode): void {
|
||||
const [x, y, width] = nodes[0].getBounding()
|
||||
batchNode.pos = [x + width + 100, y + 30]
|
||||
|
||||
Reference in New Issue
Block a user