mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-10 10:00:08 +00:00
refactor: Apply PR #6112 review feedback for Media Assets feature
- Move composables to platform/assets directory structure - Extract interface-based abstraction (IAssetsProvider) for cloud/internal implementations - Move constants to module scope to avoid re-initialization - Extract helper functions (truncateFilename, assetMappers) for reusability - Rename getMediaTypeFromFilename to return singular form (image/video/audio) - Add deprecated plural version for backward compatibility - Add comprehensive test coverage for new utility functions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -65,23 +65,18 @@ import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import VirtualGrid from '@/components/common/VirtualGrid.vue'
|
||||
import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue'
|
||||
import ResultGallery from '@/components/sidebar/tabs/queue/ResultGallery.vue'
|
||||
import { useCloudMediaAssets } from '@/composables/useCloudMediaAssets'
|
||||
import { useInternalMediaAssets } from '@/composables/useInternalMediaAssets'
|
||||
import MediaAssetCard from '@/platform/assets/components/MediaAssetCard.vue'
|
||||
import { useMediaAssets } from '@/platform/assets/composables/useMediaAssets'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { ResultItemImpl } from '@/stores/queueStore'
|
||||
import { getMediaTypeFromFilename } from '@/utils/formatUtil'
|
||||
import { getMediaTypeFromFilenamePlural } from '@/utils/formatUtil'
|
||||
|
||||
const activeTab = ref<'input' | 'output'>('input')
|
||||
const mediaAssets = ref<AssetItem[]>([])
|
||||
const selectedAsset = ref<AssetItem | null>(null)
|
||||
|
||||
// Use appropriate implementation based on environment
|
||||
const implementation = isCloud
|
||||
? useCloudMediaAssets()
|
||||
: useInternalMediaAssets()
|
||||
const { loading, error, fetchMediaList } = implementation
|
||||
// Use unified media assets implementation that handles cloud/internal automatically
|
||||
const { loading, error, fetchMediaList } = useMediaAssets()
|
||||
|
||||
const galleryActiveIndex = ref(-1)
|
||||
const galleryItems = computed(() => {
|
||||
@@ -92,7 +87,7 @@ const galleryItems = computed(() => {
|
||||
subfolder: '',
|
||||
type: 'output',
|
||||
nodeId: '0',
|
||||
mediaType: getMediaTypeFromFilename(asset.name)
|
||||
mediaType: getMediaTypeFromFilenamePlural(asset.name)
|
||||
})
|
||||
|
||||
// Override the url getter to use asset.preview_url
|
||||
|
||||
@@ -128,7 +128,7 @@ import CardBottom from '@/components/card/CardBottom.vue'
|
||||
import CardContainer from '@/components/card/CardContainer.vue'
|
||||
import CardTop from '@/components/card/CardTop.vue'
|
||||
import SquareChip from '@/components/chip/SquareChip.vue'
|
||||
import { formatDuration, getMediaKindFromFilename } from '@/utils/formatUtil'
|
||||
import { formatDuration, getMediaTypeFromFilename } from '@/utils/formatUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import { useMediaAssetActions } from '../composables/useMediaAssetActions'
|
||||
@@ -191,7 +191,7 @@ const assetType = computed(() => {
|
||||
|
||||
// Determine file type from extension
|
||||
const fileKind = computed((): MediaKind => {
|
||||
return getMediaKindFromFilename(asset?.name || '') as MediaKind
|
||||
return getMediaTypeFromFilename(asset?.name || '') as MediaKind
|
||||
})
|
||||
|
||||
// Adapt AssetItem to legacy AssetMeta format for existing components
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
|
||||
/**
|
||||
* Interface for media assets providers
|
||||
* Defines the common API for both cloud and internal file implementations
|
||||
*/
|
||||
export interface IAssetsProvider {
|
||||
/** Loading state indicator */
|
||||
loading: Ref<boolean>
|
||||
|
||||
/** Error state, null when no error */
|
||||
error: Ref<string | null>
|
||||
|
||||
/**
|
||||
* Fetch list of media assets from the specified directory
|
||||
* @param directory - 'input' or 'output'
|
||||
* @returns Promise resolving to array of AssetItem
|
||||
*/
|
||||
fetchMediaList: (directory: 'input' | 'output') => Promise<AssetItem[]>
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import type { TaskItemImpl } from '@/stores/queueStore'
|
||||
import { truncateFilename } from '@/utils/formatUtil'
|
||||
|
||||
/**
|
||||
* Maps a TaskItemImpl output to an AssetItem format
|
||||
* @param taskItem The task item containing execution data
|
||||
* @param output The output from the task
|
||||
* @param useDisplayName Whether to truncate the filename for display
|
||||
* @returns AssetItem formatted object
|
||||
*/
|
||||
export function mapTaskOutputToAssetItem(
|
||||
taskItem: TaskItemImpl,
|
||||
output: any,
|
||||
useDisplayName: boolean = false
|
||||
): AssetItem {
|
||||
const metadata: Record<string, any> = {
|
||||
promptId: taskItem.promptId,
|
||||
nodeId: output.nodeId,
|
||||
subfolder: output.subfolder
|
||||
}
|
||||
|
||||
// Add execution time if available
|
||||
if (taskItem.executionTimeInSeconds) {
|
||||
metadata.executionTimeInSeconds = taskItem.executionTimeInSeconds
|
||||
}
|
||||
|
||||
// Add format if available
|
||||
if (output.format) {
|
||||
metadata.format = output.format
|
||||
}
|
||||
|
||||
// Add workflow if available
|
||||
if (taskItem.workflow) {
|
||||
metadata.workflow = taskItem.workflow
|
||||
}
|
||||
|
||||
// Store original filename if using display name
|
||||
if (useDisplayName) {
|
||||
metadata.originalFilename = output.filename
|
||||
}
|
||||
|
||||
return {
|
||||
id: `${taskItem.promptId}-${output.nodeId}-${output.filename}`,
|
||||
name: useDisplayName
|
||||
? truncateFilename(output.filename, 20)
|
||||
: output.filename,
|
||||
size: 0, // Size not available from history API
|
||||
created_at: taskItem.executionStartTimestamp
|
||||
? new Date(taskItem.executionStartTimestamp).toISOString()
|
||||
: new Date().toISOString(),
|
||||
tags: ['output'],
|
||||
preview_url: output.url,
|
||||
user_metadata: metadata
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps input directory file to AssetItem format
|
||||
* @param filename The filename
|
||||
* @param index File index for unique ID
|
||||
* @param directory The directory type
|
||||
* @returns AssetItem formatted object
|
||||
*/
|
||||
export function mapInputFileToAssetItem(
|
||||
filename: string,
|
||||
index: number,
|
||||
directory: 'input' | 'output' = 'input'
|
||||
): AssetItem {
|
||||
return {
|
||||
id: `${directory}-${index}-${filename}`,
|
||||
name: filename,
|
||||
size: 0,
|
||||
created_at: new Date().toISOString(),
|
||||
tags: [directory],
|
||||
preview_url: api.apiURL(
|
||||
`/view?filename=${encodeURIComponent(filename)}&type=${directory}`
|
||||
)
|
||||
}
|
||||
}
|
||||
17
src/platform/assets/composables/useMediaAssets/index.ts
Normal file
17
src/platform/assets/composables/useMediaAssets/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
|
||||
import type { IAssetsProvider } from './IAssetsProvider'
|
||||
import { useAssetsApi } from './useAssetsApi'
|
||||
import { useInternalFilesApi } from './useInternalFilesApi'
|
||||
|
||||
/**
|
||||
* Factory function that returns the appropriate media assets implementation
|
||||
* based on the current distribution (cloud vs internal)
|
||||
* @returns IAssetsProvider implementation
|
||||
*/
|
||||
export function useMediaAssets(): IAssetsProvider {
|
||||
return isCloud ? useAssetsApi() : useInternalFilesApi()
|
||||
}
|
||||
|
||||
// Re-export the interface for consumers
|
||||
export type { IAssetsProvider } from './IAssetsProvider'
|
||||
@@ -6,11 +6,13 @@ import type { HistoryTaskItem } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { TaskItemImpl } from '@/stores/queueStore'
|
||||
|
||||
import { mapTaskOutputToAssetItem } from './assetMappers'
|
||||
|
||||
/**
|
||||
* Composable for fetching media assets from cloud environment
|
||||
* Includes execution time from history API
|
||||
*/
|
||||
export function useCloudMediaAssets() {
|
||||
export function useAssetsApi() {
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
@@ -53,62 +55,16 @@ export function useCloudMediaAssets() {
|
||||
|
||||
// Only process completed tasks
|
||||
if (taskItem.displayStatus === 'Completed' && taskItem.outputs) {
|
||||
// Get execution time
|
||||
const executionTimeInSeconds = taskItem.executionTimeInSeconds
|
||||
|
||||
// Process each output
|
||||
taskItem.flatOutputs.forEach((output) => {
|
||||
// Only include output type files (not temp previews)
|
||||
if (output.type === 'output' && output.supportsPreview) {
|
||||
// Truncate filename if longer than 15 characters
|
||||
let displayName = output.filename
|
||||
if (output.filename.length > 20) {
|
||||
// Get file extension
|
||||
const lastDotIndex = output.filename.lastIndexOf('.')
|
||||
const nameWithoutExt =
|
||||
lastDotIndex > -1
|
||||
? output.filename.substring(0, lastDotIndex)
|
||||
: output.filename
|
||||
const extension =
|
||||
lastDotIndex > -1
|
||||
? output.filename.substring(lastDotIndex)
|
||||
: ''
|
||||
|
||||
// If name without extension is still long, truncate it
|
||||
if (nameWithoutExt.length > 10) {
|
||||
displayName =
|
||||
nameWithoutExt.substring(0, 10) +
|
||||
'...' +
|
||||
nameWithoutExt.substring(nameWithoutExt.length - 10) +
|
||||
extension
|
||||
}
|
||||
}
|
||||
|
||||
assetItems.push({
|
||||
id: `${taskItem.promptId}-${output.nodeId}-${output.filename}`,
|
||||
name: displayName,
|
||||
size: 0, // We don't have size info from history
|
||||
created_at: taskItem.executionStartTimestamp
|
||||
? new Date(taskItem.executionStartTimestamp).toISOString()
|
||||
: new Date().toISOString(),
|
||||
tags: ['output'],
|
||||
preview_url: output.url,
|
||||
user_metadata: {
|
||||
originalFilename: output.filename, // Store original filename
|
||||
promptId: taskItem.promptId,
|
||||
nodeId: output.nodeId,
|
||||
subfolder: output.subfolder,
|
||||
...(executionTimeInSeconds && {
|
||||
executionTimeInSeconds
|
||||
}),
|
||||
...(output.format && {
|
||||
format: output.format
|
||||
}),
|
||||
...(taskItem.workflow && {
|
||||
workflow: taskItem.workflow
|
||||
})
|
||||
}
|
||||
})
|
||||
const assetItem = mapTaskOutputToAssetItem(
|
||||
taskItem,
|
||||
output,
|
||||
true // Use display name for cloud
|
||||
)
|
||||
assetItems.push(assetItem)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -5,11 +5,16 @@ import type { HistoryTaskItem } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { TaskItemImpl } from '@/stores/queueStore'
|
||||
|
||||
import {
|
||||
mapInputFileToAssetItem,
|
||||
mapTaskOutputToAssetItem
|
||||
} from './assetMappers'
|
||||
|
||||
/**
|
||||
* Composable for fetching media assets from local environment
|
||||
* Uses the same logic as QueueSidebarTab for history processing
|
||||
*/
|
||||
export function useInternalMediaAssets() {
|
||||
export function useInternalFilesApi() {
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
@@ -37,16 +42,9 @@ export function useInternalMediaAssets() {
|
||||
}
|
||||
const filenames: string[] = await response.json()
|
||||
|
||||
return filenames.map((name, index) => ({
|
||||
id: `${directory}-${index}-${name}`,
|
||||
name,
|
||||
size: 0,
|
||||
created_at: new Date().toISOString(),
|
||||
tags: [directory],
|
||||
preview_url: api.apiURL(
|
||||
`/view?filename=${encodeURIComponent(name)}&type=${directory}`
|
||||
)
|
||||
}))
|
||||
return filenames.map((name, index) =>
|
||||
mapInputFileToAssetItem(name, index, directory)
|
||||
)
|
||||
}
|
||||
|
||||
// For output directory, use history data like QueueSidebarTab
|
||||
@@ -70,37 +68,16 @@ export function useInternalMediaAssets() {
|
||||
|
||||
// Only process completed tasks
|
||||
if (taskItem.displayStatus === 'Completed' && taskItem.outputs) {
|
||||
const executionTimeInSeconds = taskItem.executionTimeInSeconds
|
||||
const executionStartTimestamp = taskItem.executionStartTimestamp
|
||||
|
||||
// Process each output using flatOutputs like QueueSidebarTab
|
||||
taskItem.flatOutputs.forEach((output) => {
|
||||
// Only include output type files (not temp previews)
|
||||
if (output.type === 'output' && output.supportsPreview) {
|
||||
assetItems.push({
|
||||
id: `${taskItem.promptId}-${output.nodeId}-${output.filename}`,
|
||||
name: output.filename,
|
||||
size: 0,
|
||||
created_at: executionStartTimestamp
|
||||
? new Date(executionStartTimestamp).toISOString()
|
||||
: new Date().toISOString(),
|
||||
tags: ['output'],
|
||||
preview_url: output.url,
|
||||
user_metadata: {
|
||||
promptId: taskItem.promptId,
|
||||
nodeId: output.nodeId,
|
||||
subfolder: output.subfolder,
|
||||
...(executionTimeInSeconds && {
|
||||
executionTimeInSeconds
|
||||
}),
|
||||
...(output.format && {
|
||||
format: output.format
|
||||
}),
|
||||
...(taskItem.workflow && {
|
||||
workflow: taskItem.workflow
|
||||
})
|
||||
}
|
||||
})
|
||||
const assetItem = mapTaskOutputToAssetItem(
|
||||
taskItem,
|
||||
output,
|
||||
false // Don't use display name for internal
|
||||
)
|
||||
assetItems.push(assetItem)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user