mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-13 09:00:16 +00:00
feat: audio drag-drop and paste support (#9152)
This commit is contained in:
@@ -5,12 +5,13 @@ import type {
|
||||
LGraphGroup,
|
||||
LGraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { app } from '@/scripts/app'
|
||||
import { createMockLGraphNode } from '@/utils/__tests__/litegraphTestUtils'
|
||||
import { createNode, isImageNode } from '@/utils/litegraphUtil'
|
||||
import { createNode, isAudioNode, isImageNode } from '@/utils/litegraphUtil'
|
||||
import {
|
||||
cloneDataTransfer,
|
||||
pasteAudioNode,
|
||||
pasteAudioNodes,
|
||||
pasteImageNode,
|
||||
pasteImageNodes,
|
||||
usePaste
|
||||
@@ -203,6 +204,102 @@ describe('pasteImageNodes', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('pasteAudioNode', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should create new LoadAudio node when no audio node provided', async () => {
|
||||
const mockNode = createMockNode()
|
||||
vi.mocked(createNode).mockResolvedValue(mockNode)
|
||||
|
||||
const file = createAudioFile()
|
||||
const dataTransfer = createDataTransfer([file])
|
||||
|
||||
await pasteAudioNode(mockCanvas, dataTransfer.items)
|
||||
|
||||
expect(createNode).toHaveBeenCalledWith(mockCanvas, 'LoadAudio')
|
||||
expect(mockNode.pasteFile).toHaveBeenCalledWith(file)
|
||||
})
|
||||
|
||||
it('should use existing audio node when provided', async () => {
|
||||
const mockNode = createMockNode()
|
||||
const file = createAudioFile()
|
||||
const dataTransfer = createDataTransfer([file])
|
||||
|
||||
await pasteAudioNode(mockCanvas, dataTransfer.items, mockNode)
|
||||
|
||||
expect(createNode).not.toHaveBeenCalled()
|
||||
expect(mockNode.pasteFile).toHaveBeenCalledWith(file)
|
||||
})
|
||||
|
||||
it('should filter non-audio items', async () => {
|
||||
const mockNode = createMockNode()
|
||||
const audioFile = createAudioFile()
|
||||
const textFile = new File([''], 'test.txt', { type: 'text/plain' })
|
||||
const dataTransfer = createDataTransfer([textFile, audioFile])
|
||||
|
||||
await pasteAudioNode(mockCanvas, dataTransfer.items, mockNode)
|
||||
|
||||
expect(mockNode.pasteFile).toHaveBeenCalledWith(audioFile)
|
||||
expect(mockNode.pasteFiles).toHaveBeenCalledWith([audioFile])
|
||||
})
|
||||
|
||||
it('should do nothing when no audio files present', async () => {
|
||||
const mockNode = createMockNode()
|
||||
const dataTransfer = createDataTransfer()
|
||||
|
||||
await pasteAudioNode(mockCanvas, dataTransfer.items, mockNode)
|
||||
|
||||
expect(mockNode.pasteFile).not.toHaveBeenCalled()
|
||||
expect(mockNode.pasteFiles).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('pasteAudioNodes', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should create multiple nodes for multiple audio files', async () => {
|
||||
const mockNode1 = createMockNode()
|
||||
const mockNode2 = createMockNode()
|
||||
vi.mocked(createNode)
|
||||
.mockResolvedValueOnce(mockNode1)
|
||||
.mockResolvedValueOnce(mockNode2)
|
||||
|
||||
const file1 = createAudioFile('file1.mp3')
|
||||
const file2 = createAudioFile('file2.wav', 'audio/wav')
|
||||
|
||||
const result = await pasteAudioNodes(mockCanvas, [file1, file2])
|
||||
|
||||
expect(createNode).toHaveBeenCalledTimes(2)
|
||||
expect(createNode).toHaveBeenNthCalledWith(1, mockCanvas, 'LoadAudio')
|
||||
expect(createNode).toHaveBeenNthCalledWith(2, mockCanvas, 'LoadAudio')
|
||||
expect(mockNode1.pasteFile).toHaveBeenCalledWith(file1)
|
||||
expect(mockNode2.pasteFile).toHaveBeenCalledWith(file2)
|
||||
expect(result).toEqual([mockNode1, mockNode2])
|
||||
})
|
||||
|
||||
it('should handle empty file list', async () => {
|
||||
const result = await pasteAudioNodes(mockCanvas, [])
|
||||
|
||||
expect(createNode).not.toHaveBeenCalled()
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('should handle single audio file', async () => {
|
||||
const mockNode = createMockNode()
|
||||
vi.mocked(createNode).mockResolvedValue(mockNode)
|
||||
|
||||
const file = createAudioFile()
|
||||
const result = await pasteAudioNodes(mockCanvas, [file])
|
||||
|
||||
expect(createNode).toHaveBeenCalledTimes(1)
|
||||
expect(result).toEqual([mockNode])
|
||||
})
|
||||
})
|
||||
|
||||
describe('usePaste', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -230,9 +327,9 @@ describe('usePaste', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle audio paste', async () => {
|
||||
it('should handle audio paste using createNode helper', async () => {
|
||||
const mockNode = createMockNode()
|
||||
vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode)
|
||||
vi.mocked(createNode).mockResolvedValue(mockNode)
|
||||
|
||||
usePaste()
|
||||
|
||||
@@ -242,7 +339,29 @@ describe('usePaste', () => {
|
||||
document.dispatchEvent(event)
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(LiteGraph.createNode).toHaveBeenCalledWith('LoadAudio')
|
||||
expect(createNode).toHaveBeenCalledWith(mockCanvas, 'LoadAudio')
|
||||
expect(mockNode.pasteFile).toHaveBeenCalledWith(file)
|
||||
})
|
||||
})
|
||||
|
||||
it('should paste audio onto selected LoadAudio node', async () => {
|
||||
const mockNode = createMockLGraphNode({
|
||||
is_selected: true,
|
||||
pasteFile: vi.fn(),
|
||||
pasteFiles: vi.fn()
|
||||
})
|
||||
mockCanvas.current_node = mockNode
|
||||
vi.mocked(isAudioNode).mockReturnValue(true)
|
||||
|
||||
usePaste()
|
||||
|
||||
const file = createAudioFile()
|
||||
const dataTransfer = createDataTransfer([file])
|
||||
const event = new ClipboardEvent('paste', { clipboardData: dataTransfer })
|
||||
document.dispatchEvent(event)
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(createNode).not.toHaveBeenCalled()
|
||||
expect(mockNode.pasteFile).toHaveBeenCalledWith(file)
|
||||
})
|
||||
})
|
||||
@@ -273,7 +392,7 @@ describe('usePaste', () => {
|
||||
const event = new ClipboardEvent('paste', { clipboardData: dataTransfer })
|
||||
document.dispatchEvent(event)
|
||||
|
||||
expect(LiteGraph.createNode).not.toHaveBeenCalled()
|
||||
expect(createNode).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should use existing image node when selected', () => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
|
||||
import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { app } from '@/scripts/app'
|
||||
@@ -113,6 +112,37 @@ export async function pasteImageNodes(
|
||||
return nodes
|
||||
}
|
||||
|
||||
export async function pasteAudioNode(
|
||||
canvas: LGraphCanvas,
|
||||
items: DataTransferItemList,
|
||||
audioNode: LGraphNode | null = null
|
||||
): Promise<LGraphNode | null> {
|
||||
if (!audioNode) {
|
||||
audioNode = await createNode(canvas, 'LoadAudio')
|
||||
}
|
||||
pasteItemsOnNode(items, audioNode, 'audio')
|
||||
return audioNode
|
||||
}
|
||||
|
||||
export async function pasteAudioNodes(
|
||||
canvas: LGraphCanvas,
|
||||
fileList: File[]
|
||||
): Promise<LGraphNode[]> {
|
||||
const nodes: LGraphNode[] = []
|
||||
|
||||
for (const file of fileList) {
|
||||
const transfer = new DataTransfer()
|
||||
transfer.items.add(file)
|
||||
const node = await pasteAudioNode(canvas, transfer.items)
|
||||
|
||||
if (node) {
|
||||
nodes.push(node)
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a handler on paste that extracts and loads images or workflows from pasted JSON data
|
||||
*/
|
||||
@@ -132,7 +162,6 @@ export const usePaste = () => {
|
||||
const { canvas } = canvasStore
|
||||
if (!canvas) return
|
||||
|
||||
const { graph } = canvas
|
||||
let data: DataTransfer | string | null = e.clipboardData
|
||||
if (!data) throw new Error('No clipboard data on clipboard event')
|
||||
data = cloneDataTransfer(data)
|
||||
@@ -146,7 +175,9 @@ export const usePaste = () => {
|
||||
const isVideoNodeSelected = isNodeSelected && isVideoNode(currentNode)
|
||||
const isAudioNodeSelected = isNodeSelected && isAudioNode(currentNode)
|
||||
|
||||
let audioNode: LGraphNode | null = isAudioNodeSelected ? currentNode : null
|
||||
const audioNode: LGraphNode | null = isAudioNodeSelected
|
||||
? currentNode
|
||||
: null
|
||||
const imageNode: LGraphNode | null = isImageNodeSelected
|
||||
? currentNode
|
||||
: null
|
||||
@@ -168,16 +199,7 @@ export const usePaste = () => {
|
||||
return
|
||||
}
|
||||
} else if (item.type.startsWith('audio/')) {
|
||||
if (!audioNode) {
|
||||
// No audio node selected: add a new one
|
||||
const newNode = LiteGraph.createNode('LoadAudio')
|
||||
if (newNode) {
|
||||
newNode.pos = [canvas.graph_mouse[0], canvas.graph_mouse[1]]
|
||||
audioNode = graph?.add(newNode) ?? null
|
||||
}
|
||||
graph?.change()
|
||||
}
|
||||
pasteItemsOnNode(items, audioNode, 'audio')
|
||||
await pasteAudioNode(canvas as LGraphCanvas, items, audioNode)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user