mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-27 18:24:11 +00:00
## 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>
144 lines
3.7 KiB
TypeScript
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 }
|
|
}
|