mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
## 📋 Overview Implemented a new Media Assets sidebar tab in ComfyUI for managing user-uploaded input files and generated output files. This feature supports both local and cloud environments and is currently enabled only in development mode. ## 🎯 Key Features ### 1. Media Assets Sidebar Tab - **Imported** / **Generated** files separated by tabs - Visual display with file preview cards - Gallery view support (navigable with arrow keys) ### 2. Environment-Specific Implementation - **`useInternalMediaAssets`**: For local environment - Fetches file list via `/files` API - Retrieves generation task execution time via `/history` API - Processes history data using the same logic as QueueSidebarTab - **`useCloudMediaAssets`**: For cloud environment - File retrieval through assetService - History data processing using TaskItemImpl - Auto-truncation of long filenames over 20 characters (e.g., `very_long_filename_here.png` → `very_long_...here.png`) ### 3. Execution Time Display - Shows task execution time on generated image cards (e.g., "2.3s") - Calculated from History API's `execution_start` and `execution_success` messages - Displayed at MediaAssetCard's duration chip location ### 4. Gallery Feature - Full-screen gallery mode on image click - Navigate between images with keyboard arrows - Exit gallery with ESC key - Reuses ResultGallery component from QueueSidebarTab ### 5. Development Mode Only - Excluded from production builds using `import.meta.env.DEV` condition - Feature in development, scheduled for official release after stabilization ## 🛠️ Technical Changes ### New Files Added - `src/components/sidebar/tabs/AssetsSidebarTab.vue` - Main sidebar tab component - `src/composables/sidebarTabs/useAssetsSidebarTab.ts` - Sidebar tab definition - `src/composables/useInternalMediaAssets.ts` - Local environment implementation - `src/composables/useCloudMediaAssets.ts` - Cloud environment implementation - `packages/design-system/src/icons/image-ai-edit.svg` - Icon addition ### Modified Files - `src/stores/workspace/sidebarTabStore.ts` - Added dev mode only tab display logic - `src/platform/assets/components/MediaAssetCard.vue` - Added execution time display, zoom event - `src/platform/assets/components/MediaImageTop.vue` - Added image dimension detection - `packages/shared-frontend-utils/src/formatUtil.ts` - Added media type determination utility functions - `src/locales/en/main.json` - Added translation keys [media_asset_OSS_cloud.webm](https://github.com/user-attachments/assets/a6ee3b49-19ed-4735-baad-c2ac2da868ef) --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: github-actions <github-actions@github.com>
122 lines
3.2 KiB
TypeScript
122 lines
3.2 KiB
TypeScript
import { useNodeDragAndDrop } from '@/composables/node/useNodeDragAndDrop'
|
|
import { useNodeFileInput } from '@/composables/node/useNodeFileInput'
|
|
import { useNodePaste } from '@/composables/node/useNodePaste'
|
|
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
|
|
}
|
|
|
|
/**
|
|
* 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[]) => {
|
|
const paths = await Promise.all(files.map(handleUpload))
|
|
const validPaths = paths.filter((p): p is string => !!p)
|
|
if (validPaths.length) onUploadComplete(validPaths)
|
|
return validPaths
|
|
}
|
|
|
|
// 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 }
|
|
}
|