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:
Jin Yi
2025-10-20 17:40:47 +09:00
parent 2398e26712
commit dcd6bb6519
9 changed files with 368 additions and 131 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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[]>
}

View File

@@ -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}`
)
}
}

View 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'

View File

@@ -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)
}
})
}

View File

@@ -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)
}
})
}