Skip to content

Commit 31e405a

Browse files
DrJKLactions-user
andauthored
Feat: Loading state while loading dropped workflows (#6464)
## Summary Indicate to the user that we're hard at work parsing their JSON behind the scenes. ## Changes - **What**: Turn on the loading spinner while processing a workflow - **What else**: Refactored the code around figuring out how to grab the data from the file to make this easier ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6464-WIP-Loading-state-for-dropped-workflows-29c6d73d3650812dba66f2a7d27a777c) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action <[email protected]>
1 parent 0e9c29e commit 31e405a

File tree

8 files changed

+287
-263
lines changed

8 files changed

+287
-263
lines changed

src/scripts/app.ts

Lines changed: 77 additions & 254 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useResizeObserver } from '@vueuse/core'
1+
import { useEventListener, useResizeObserver } from '@vueuse/core'
22
import _ from 'es-toolkit/compat'
33
import type { ToastMessageOptions } from 'primevue/toast'
44
import { reactive, unref } from 'vue'
@@ -42,12 +42,6 @@ import {
4242
isComboInputSpecV2
4343
} from '@/schemas/nodeDefSchema'
4444
import { type BaseDOMWidget, DOMWidgetImpl } from '@/scripts/domWidget'
45-
import { getFromWebmFile } from '@/scripts/metadata/ebml'
46-
import { getGltfBinaryMetadata } from '@/scripts/metadata/gltf'
47-
import { getFromIsobmffFile } from '@/scripts/metadata/isobmff'
48-
import { getMp3Metadata } from '@/scripts/metadata/mp3'
49-
import { getOggMetadata } from '@/scripts/metadata/ogg'
50-
import { getSvgMetadata } from '@/scripts/metadata/svg'
5145
import { useDialogService } from '@/services/dialogService'
5246
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
5347
import { useExtensionService } from '@/services/extensionService'
@@ -89,19 +83,14 @@ import { deserialiseAndCreate } from '@/utils/vintageClipboard'
8983

9084
import { type ComfyApi, PromptExecutionError, api } from './api'
9185
import { defaultGraph } from './defaultGraph'
92-
import {
93-
getAvifMetadata,
94-
getFlacMetadata,
95-
getLatentMetadata,
96-
getPngMetadata,
97-
getWebpMetadata,
98-
importA1111
99-
} from './pnginfo'
86+
import { importA1111 } from './pnginfo'
10087
import { $el, ComfyUI } from './ui'
10188
import { ComfyAppMenu } from './ui/menu/index'
10289
import { clone } from './utils'
10390
import { type ComfyWidgetConstructor } from './widgets'
10491
import { ensureCorrectLayoutScale } from '@/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale'
92+
import { extractFileFromDragEvent } from '@/utils/eventUtils'
93+
import { getWorkflowDataFromFile } from '@/scripts/metadata/parser'
10594

10695
export const ANIM_PREVIEW_WIDGET = '$$comfy_animation_preview'
10796

@@ -534,7 +523,7 @@ export class ComfyApp {
534523
*/
535524
private addDropHandler() {
536525
// Get prompt from dropped PNG or json
537-
document.addEventListener('drop', async (event) => {
526+
useEventListener(document, 'drop', async (event: DragEvent) => {
538527
try {
539528
event.preventDefault()
540529
event.stopPropagation()
@@ -543,66 +532,49 @@ export class ComfyApp {
543532
this.dragOverNode = null
544533
// Node handles file drop, we dont use the built in onDropFile handler as its buggy
545534
// If you drag multiple files it will call it multiple times with the same file
546-
if (n && n.onDragDrop && (await n.onDragDrop(event))) {
547-
return
548-
}
549-
// Dragging from Chrome->Firefox there is a file but its a bmp, so ignore that
550-
if (!event.dataTransfer) return
551-
if (
552-
event.dataTransfer.files.length &&
553-
event.dataTransfer.files[0].type !== 'image/bmp'
554-
) {
555-
await this.handleFile(event.dataTransfer.files[0], 'file_drop')
556-
} else {
557-
// Try loading the first URI in the transfer list
558-
const validTypes = ['text/uri-list', 'text/x-moz-url']
559-
const match = [...event.dataTransfer.types].find((t) =>
560-
validTypes.find((v) => t === v)
561-
)
562-
if (match) {
563-
const uri = event.dataTransfer.getData(match)?.split('\n')?.[0]
564-
if (uri) {
565-
const blob = await (await fetch(uri)).blob()
566-
await this.handleFile(
567-
new File([blob], uri, { type: blob.type }),
568-
'file_drop'
569-
)
570-
}
571-
}
535+
if (await n?.onDragDrop?.(event)) return
536+
537+
const fileMaybe = await extractFileFromDragEvent(event)
538+
if (!fileMaybe) return
539+
540+
const workspace = useWorkspaceStore()
541+
try {
542+
workspace.spinner = true
543+
await this.handleFile(fileMaybe, 'file_drop')
544+
} finally {
545+
workspace.spinner = false
572546
}
573-
} catch (err: any) {
574-
useToastStore().addAlert(
575-
t('toastMessages.dropFileError', { error: err })
576-
)
547+
} catch (error: unknown) {
548+
useToastStore().addAlert(t('toastMessages.dropFileError', { error }))
577549
}
578550
})
579551

580552
// Always clear over node on drag leave
581-
this.canvasEl.addEventListener('dragleave', async () => {
582-
if (this.dragOverNode) {
583-
this.dragOverNode = null
584-
this.graph.setDirtyCanvas(false, true)
585-
}
553+
useEventListener(this.canvasElRef, 'dragleave', async () => {
554+
if (!this.dragOverNode) return
555+
this.dragOverNode = null
556+
this.graph.setDirtyCanvas(false, true)
586557
})
587558

588559
// Add handler for dropping onto a specific node
589-
this.canvasEl.addEventListener(
560+
useEventListener(
561+
this.canvasElRef,
590562
'dragover',
591-
(e) => {
592-
this.canvas.adjustMouseEvent(e)
593-
const node = this.graph.getNodeOnPos(e.canvasX, e.canvasY)
594-
if (node) {
595-
if (node.onDragOver && node.onDragOver(e)) {
596-
this.dragOverNode = node
597-
598-
// dragover event is fired very frequently, run this on an animation frame
599-
requestAnimationFrame(() => {
600-
this.graph.setDirtyCanvas(false, true)
601-
})
602-
return
603-
}
563+
(event: DragEvent) => {
564+
this.canvas.adjustMouseEvent(event)
565+
const node = this.graph.getNodeOnPos(event.canvasX, event.canvasY)
566+
567+
if (!node?.onDragOver?.(event)) {
568+
this.dragOverNode = null
569+
return
604570
}
605-
this.dragOverNode = null
571+
572+
this.dragOverNode = node
573+
574+
// dragover event is fired very frequently, run this on an animation frame
575+
requestAnimationFrame(() => {
576+
this.graph.setDirtyCanvas(false, true)
577+
})
606578
},
607579
false
608580
)
@@ -1417,199 +1389,50 @@ export class ComfyApp {
14171389
* @param {File} file
14181390
*/
14191391
async handleFile(file: File, openSource?: WorkflowOpenSource) {
1420-
const removeExt = (f: string) => {
1421-
if (!f) return f
1422-
const p = f.lastIndexOf('.')
1423-
if (p === -1) return f
1424-
return f.substring(0, p)
1392+
const fileName = file.name.replace(/\.\w+$/, '') // Strip file extension
1393+
const workflowData = await getWorkflowDataFromFile(file)
1394+
if (!workflowData) {
1395+
this.showErrorOnFileLoad(file)
1396+
return
14251397
}
1426-
const fileName = removeExt(file.name)
1427-
if (file.type === 'image/png') {
1428-
const pngInfo = await getPngMetadata(file)
1429-
if (pngInfo?.workflow) {
1430-
await this.loadGraphData(
1431-
JSON.parse(pngInfo.workflow),
1432-
true,
1433-
true,
1434-
fileName,
1435-
{ openSource }
1436-
)
1437-
} else if (pngInfo?.prompt) {
1438-
this.loadApiJson(JSON.parse(pngInfo.prompt), fileName)
1439-
} else if (pngInfo?.parameters) {
1440-
// Note: Not putting this in `importA1111` as it is mostly not used
1441-
// by external callers, and `importA1111` has no access to `app`.
1442-
useWorkflowService().beforeLoadNewGraph()
1443-
importA1111(this.graph, pngInfo.parameters)
1444-
useWorkflowService().afterLoadNewGraph(
1445-
fileName,
1446-
this.graph.serialize() as unknown as ComfyWorkflowJSON
1447-
)
1448-
} else {
1449-
this.showErrorOnFileLoad(file)
1450-
}
1451-
} else if (file.type === 'image/avif') {
1452-
const { workflow, prompt } = await getAvifMetadata(file)
14531398

1454-
if (workflow) {
1455-
this.loadGraphData(JSON.parse(workflow), true, true, fileName, {
1456-
openSource
1457-
})
1458-
} else if (prompt) {
1459-
this.loadApiJson(JSON.parse(prompt), fileName)
1460-
} else {
1461-
this.showErrorOnFileLoad(file)
1462-
}
1463-
} else if (file.type === 'image/webp') {
1464-
const pngInfo = await getWebpMetadata(file)
1465-
// Support loading workflows from that webp custom node.
1466-
const workflow = pngInfo?.workflow || pngInfo?.Workflow
1467-
const prompt = pngInfo?.prompt || pngInfo?.Prompt
1468-
1469-
if (workflow) {
1470-
this.loadGraphData(JSON.parse(workflow), true, true, fileName, {
1471-
openSource
1472-
})
1473-
} else if (prompt) {
1474-
this.loadApiJson(JSON.parse(prompt), fileName)
1475-
} else {
1476-
this.showErrorOnFileLoad(file)
1477-
}
1478-
} else if (file.type === 'audio/mpeg') {
1479-
const { workflow, prompt } = await getMp3Metadata(file)
1480-
if (workflow) {
1481-
this.loadGraphData(workflow, true, true, fileName, { openSource })
1482-
} else if (prompt) {
1483-
this.loadApiJson(prompt, fileName)
1484-
} else {
1485-
this.showErrorOnFileLoad(file)
1486-
}
1487-
} else if (file.type === 'audio/ogg') {
1488-
const { workflow, prompt } = await getOggMetadata(file)
1489-
if (workflow) {
1490-
this.loadGraphData(workflow, true, true, fileName, { openSource })
1491-
} else if (prompt) {
1492-
this.loadApiJson(prompt, fileName)
1493-
} else {
1494-
this.showErrorOnFileLoad(file)
1495-
}
1496-
} else if (file.type === 'audio/flac' || file.type === 'audio/x-flac') {
1497-
const pngInfo = await getFlacMetadata(file)
1498-
const workflow = pngInfo?.workflow || pngInfo?.Workflow
1499-
const prompt = pngInfo?.prompt || pngInfo?.Prompt
1500-
1501-
if (workflow) {
1502-
this.loadGraphData(JSON.parse(workflow), true, true, fileName, {
1503-
openSource
1504-
})
1505-
} else if (prompt) {
1506-
this.loadApiJson(JSON.parse(prompt), fileName)
1507-
} else {
1508-
this.showErrorOnFileLoad(file)
1509-
}
1510-
} else if (file.type === 'video/webm') {
1511-
const webmInfo = await getFromWebmFile(file)
1512-
if (webmInfo.workflow) {
1513-
this.loadGraphData(webmInfo.workflow, true, true, fileName, {
1514-
openSource
1515-
})
1516-
} else if (webmInfo.prompt) {
1517-
this.loadApiJson(webmInfo.prompt, fileName)
1518-
} else {
1519-
this.showErrorOnFileLoad(file)
1520-
}
1521-
} else if (
1522-
file.type === 'video/mp4' ||
1523-
file.name?.endsWith('.mp4') ||
1524-
file.name?.endsWith('.mov') ||
1525-
file.name?.endsWith('.m4v') ||
1526-
file.type === 'video/quicktime' ||
1527-
file.type === 'video/x-m4v'
1528-
) {
1529-
const mp4Info = await getFromIsobmffFile(file)
1530-
if (mp4Info.workflow) {
1531-
this.loadGraphData(mp4Info.workflow, true, true, fileName, {
1532-
openSource
1533-
})
1534-
} else if (mp4Info.prompt) {
1535-
this.loadApiJson(mp4Info.prompt, fileName)
1536-
}
1537-
} else if (file.type === 'image/svg+xml' || file.name?.endsWith('.svg')) {
1538-
const svgInfo = await getSvgMetadata(file)
1539-
if (svgInfo.workflow) {
1540-
this.loadGraphData(svgInfo.workflow, true, true, fileName, {
1541-
openSource
1542-
})
1543-
} else if (svgInfo.prompt) {
1544-
this.loadApiJson(svgInfo.prompt, fileName)
1545-
} else {
1546-
this.showErrorOnFileLoad(file)
1547-
}
1548-
} else if (
1549-
file.type === 'model/gltf-binary' ||
1550-
file.name?.endsWith('.glb')
1551-
) {
1552-
const gltfInfo = await getGltfBinaryMetadata(file)
1553-
if (gltfInfo.workflow) {
1554-
this.loadGraphData(gltfInfo.workflow, true, true, fileName, {
1555-
openSource
1556-
})
1557-
} else if (gltfInfo.prompt) {
1558-
this.loadApiJson(gltfInfo.prompt, fileName)
1559-
} else {
1560-
this.showErrorOnFileLoad(file)
1561-
}
1562-
} else if (
1563-
file.type === 'application/json' ||
1564-
file.name?.endsWith('.json')
1565-
) {
1566-
const reader = new FileReader()
1567-
reader.onload = async () => {
1568-
const readerResult = reader.result as string
1569-
const jsonContent = JSON.parse(readerResult)
1570-
if (jsonContent?.templates) {
1571-
this.loadTemplateData(jsonContent)
1572-
} else if (this.isApiJson(jsonContent)) {
1573-
this.loadApiJson(jsonContent, fileName)
1574-
} else {
1575-
await this.loadGraphData(
1576-
JSON.parse(readerResult),
1577-
true,
1578-
true,
1579-
fileName,
1580-
{ openSource }
1581-
)
1582-
}
1583-
}
1584-
reader.readAsText(file)
1585-
} else if (
1586-
file.name?.endsWith('.latent') ||
1587-
file.name?.endsWith('.safetensors')
1588-
) {
1589-
const info = await getLatentMetadata(file)
1590-
// TODO define schema to LatentMetadata
1591-
// @ts-expect-error
1592-
if (info.workflow) {
1593-
await this.loadGraphData(
1594-
// @ts-expect-error
1595-
JSON.parse(info.workflow),
1596-
true,
1597-
true,
1598-
fileName,
1599-
{ openSource }
1600-
)
1601-
// @ts-expect-error
1602-
} else if (info.prompt) {
1603-
// @ts-expect-error
1604-
this.loadApiJson(JSON.parse(info.prompt))
1605-
} else {
1606-
this.showErrorOnFileLoad(file)
1607-
}
1608-
} else {
1609-
this.showErrorOnFileLoad(file)
1399+
const { workflow, prompt, parameters, templates } = workflowData
1400+
1401+
if (templates) {
1402+
this.loadTemplateData({ templates })
16101403
}
1404+
1405+
if (parameters) {
1406+
// Note: Not putting this in `importA1111` as it is mostly not used
1407+
// by external callers, and `importA1111` has no access to `app`.
1408+
useWorkflowService().beforeLoadNewGraph()
1409+
importA1111(this.graph, parameters)
1410+
useWorkflowService().afterLoadNewGraph(
1411+
fileName,
1412+
this.graph.serialize() as unknown as ComfyWorkflowJSON
1413+
)
1414+
return
1415+
}
1416+
1417+
if (workflow) {
1418+
const workflowObj =
1419+
typeof workflow === 'string' ? JSON.parse(workflow) : workflow
1420+
await this.loadGraphData(workflowObj, true, true, fileName, {
1421+
openSource
1422+
})
1423+
return
1424+
}
1425+
1426+
if (prompt) {
1427+
const promptObj = typeof prompt === 'string' ? JSON.parse(prompt) : prompt
1428+
this.loadApiJson(promptObj, fileName)
1429+
return
1430+
}
1431+
1432+
this.showErrorOnFileLoad(file)
16111433
}
16121434

1435+
// @deprecated
16131436
isApiJson(data: unknown) {
16141437
return _.isObject(data) && Object.values(data).every((v) => v.class_type)
16151438
}

0 commit comments

Comments
 (0)