Files
ComfyUI_frontend/src/composables/node/useNodeImageUpload.ts
Dante c24c4ab607 feat: show loading spinner and uploading filename during image upload (#9189)
## Summary
- Show a canvas-based loading spinner on image upload nodes (LoadImage)
during file upload via drag-drop, paste, or file picker
- Display the uploading file's name immediately in the filename dropdown
instead of showing the previous file's name
- Show the uploading audio file's name immediately in the audio widget
during upload

## Changes
- **`useNodeImageUpload.ts`**: Add `isUploading` flag and
`onUploadStart` callback to the upload lifecycle; clear `node.imgs`
during upload to prevent stale previews
- **`useImagePreviewWidget.ts`**: Add `renderUploadSpinner` that draws
an animated arc spinner on the canvas when `node.isUploading` is true;
guard against empty `imgs` array
- **`useImageUploadWidget.ts`**: Set `fileComboWidget.value` to the new
filename on upload start; clear `node.imgs` on combo widget change
- **`uploadAudio.ts`**: Set `audioWidget.value` to the new filename on
upload start
- **`litegraph-augmentation.d.ts`**: Add `isUploading` property to
`LGraphNode`



https://github.com/user-attachments/assets/818ce529-cb83-428a-8c98-dd900a128343



## Test plan
- [x] Upload an image via file picker on LoadImage node — spinner shows
during upload, filename updates immediately
- [x] Drag-and-drop an image onto LoadImage node — same behavior
- [x] Paste an image onto LoadImage node — same behavior
- [x] Change the dropdown selection on LoadImage — old preview clears,
new image loads
- [x] Upload an audio file — filename updates immediately in the widget

🤖 Generated with [Claude Code](https://claude.com/claude-code)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9189-feat-show-loading-spinner-and-uploading-filename-during-image-upload-3126d73d365081e4af27cd7252f34298)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 20:22:42 -08:00

144 lines
3.7 KiB
TypeScript

import { useNodeDragAndDrop } from '@/composables/node/useNodeDragAndDrop'
import { useNodeFileInput } from '@/composables/node/useNodeFileInput'
import { useNodePaste } from '@/composables/node/useNodePaste'
import { t } from '@/i18n'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useToastStore } from '@/platform/updates/common/toastStore'
import type { ResultItemType } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { useAssetsStore } from '@/stores/assetsStore'
const PASTED_IMAGE_EXPIRY_MS = 2000
interface ImageUploadFormFields {
/**
* The folder to upload the file to.
* @example 'input', 'output', 'temp'
*/
type: ResultItemType
}
const uploadFile = async (
file: File,
isPasted: boolean,
formFields: Partial<ImageUploadFormFields> = {}
) => {
const body = new FormData()
body.append('image', file)
if (isPasted) body.append('subfolder', 'pasted')
if (formFields.type) body.append('type', formFields.type)
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
body
})
if (resp.status !== 200) {
useToastStore().addAlert(resp.status + ' - ' + resp.statusText)
return
}
const data = await resp.json()
// Update AssetsStore input assets when files are uploaded to input folder
if (formFields.type === 'input' || (!formFields.type && !isPasted)) {
const assetsStore = useAssetsStore()
await assetsStore.updateInputs()
}
return data.subfolder ? `${data.subfolder}/${data.name}` : data.name
}
interface ImageUploadOptions {
fileFilter?: (file: File) => boolean
onUploadComplete: (paths: string[]) => void
allow_batch?: boolean
/**
* The file types to accept.
* @example 'image/png,image/jpeg,image/webp,video/webm,video/mp4'
*/
accept?: string
/**
* The folder to upload the file to.
* @example 'input', 'output', 'temp'
*/
folder?: ResultItemType
onUploadStart?: (files: File[]) => void
onUploadError?: () => void
}
/**
* Adds image upload to a node via drag & drop, paste, and file input.
*/
export const useNodeImageUpload = (
node: LGraphNode,
options: ImageUploadOptions
) => {
const { fileFilter, onUploadComplete, allow_batch, accept } = options
const isPastedFile = (file: File): boolean =>
file.name === 'image.png' &&
file.lastModified - Date.now() < PASTED_IMAGE_EXPIRY_MS
const handleUpload = async (file: File) => {
try {
const path = await uploadFile(file, isPastedFile(file), {
type: options.folder
})
if (!path) return
return path
} catch (error) {
useToastStore().addAlert(String(error))
}
}
const handleUploadBatch = async (files: File[]) => {
if (node.isUploading) {
useToastStore().addAlert(t('g.uploadAlreadyInProgress'))
return []
}
node.isUploading = true
try {
node.imgs = undefined
node.graph?.setDirtyCanvas(true)
options.onUploadStart?.(files)
const paths = await Promise.all(files.map(handleUpload))
const validPaths = paths.filter((p): p is string => !!p)
if (validPaths.length) {
onUploadComplete(validPaths)
} else {
options.onUploadError?.()
}
return validPaths
} finally {
node.isUploading = false
node.graph?.setDirtyCanvas(true)
}
}
// Handle drag & drop
useNodeDragAndDrop(node, {
fileFilter,
onDrop: handleUploadBatch
})
// Handle paste
useNodePaste(node, {
fileFilter,
allow_batch,
onPaste: handleUploadBatch
})
// Handle file input
const { openFileSelection } = useNodeFileInput(node, {
fileFilter,
allow_batch,
accept,
onSelect: handleUploadBatch
})
return { openFileSelection, handleUpload }
}