Skip to content

Commit 1868ebc

Browse files
viva-jinyiclaude
andcommitted
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 <[email protected]>
1 parent 1d9ef3e commit 1868ebc

File tree

9 files changed

+368
-131
lines changed

9 files changed

+368
-131
lines changed

packages/shared-frontend-utils/src/formatUtil.ts

Lines changed: 70 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -475,50 +475,92 @@ export function formatDuration(milliseconds: number): string {
475475
return parts.join(' ')
476476
}
477477

478+
// Module scope constants to avoid re-initialization on every call
479+
const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp']
480+
const VIDEO_EXTENSIONS = ['mp4', 'webm', 'mov', 'avi']
481+
const AUDIO_EXTENSIONS = ['mp3', 'wav', 'ogg', 'flac']
482+
const THREE_D_EXTENSIONS = ['obj', 'fbx', 'gltf', 'glb']
483+
478484
/**
479-
* Determines the media type from a filename's extension
480-
* @param filename The filename to analyze
481-
* @returns The media type: 'images', 'videos', 'audios', '3D' for gallery compatibility
485+
* Truncates a filename while preserving the extension
486+
* @param filename The filename to truncate
487+
* @param maxLength Maximum length for the filename without extension
488+
* @returns Truncated filename with extension preserved
482489
*/
483-
export function getMediaTypeFromFilename(filename: string): string {
484-
if (!filename) return 'images'
485-
const ext = filename.split('.').pop()?.toLowerCase()
486-
if (!ext) return 'images'
490+
export function truncateFilename(
491+
filename: string,
492+
maxLength: number = 20
493+
): string {
494+
if (!filename || filename.length <= maxLength) {
495+
return filename
496+
}
497+
498+
const lastDotIndex = filename.lastIndexOf('.')
499+
const nameWithoutExt =
500+
lastDotIndex > -1 ? filename.substring(0, lastDotIndex) : filename
501+
const extension = lastDotIndex > -1 ? filename.substring(lastDotIndex) : ''
487502

488-
const imageExts = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp']
489-
const videoExts = ['mp4', 'webm', 'mov', 'avi']
490-
const audioExts = ['mp3', 'wav', 'ogg', 'flac']
491-
const threeDExts = ['obj', 'fbx', 'gltf', 'glb']
503+
// If the name without extension is short enough, return as is
504+
if (nameWithoutExt.length <= maxLength) {
505+
return filename
506+
}
492507

493-
if (imageExts.includes(ext)) return 'images'
494-
if (videoExts.includes(ext)) return 'videos'
495-
if (audioExts.includes(ext)) return 'audios'
496-
if (threeDExts.includes(ext)) return '3D'
508+
// Calculate how to split the truncation
509+
const halfLength = Math.floor((maxLength - 3) / 2) // -3 for '...'
510+
const start = nameWithoutExt.substring(0, halfLength)
511+
const end = nameWithoutExt.substring(nameWithoutExt.length - halfLength)
497512

498-
return 'images'
513+
return `${start}...${end}${extension}`
499514
}
500515

501516
/**
502-
* Determines the media kind from a filename's extension
517+
* Determines the media type from a filename's extension (singular form)
503518
* @param filename The filename to analyze
504-
* @returns The media kind: 'image', 'video', 'audio', or '3D'
519+
* @returns The media type: 'image', 'video', 'audio', or '3D'
505520
*/
506-
export function getMediaKindFromFilename(
521+
export function getMediaTypeFromFilename(
507522
filename: string
508523
): 'image' | 'video' | 'audio' | '3D' {
509524
if (!filename) return 'image'
510525
const ext = filename.split('.').pop()?.toLowerCase()
511526
if (!ext) return 'image'
512527

513-
const imageExts = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp']
514-
const videoExts = ['mp4', 'webm', 'mov', 'avi']
515-
const audioExts = ['mp3', 'wav', 'ogg', 'flac']
516-
const threeDExts = ['obj', 'fbx', 'gltf', 'glb']
517-
518-
if (imageExts.includes(ext)) return 'image'
519-
if (videoExts.includes(ext)) return 'video'
520-
if (audioExts.includes(ext)) return 'audio'
521-
if (threeDExts.includes(ext)) return '3D'
528+
if (IMAGE_EXTENSIONS.includes(ext)) return 'image'
529+
if (VIDEO_EXTENSIONS.includes(ext)) return 'video'
530+
if (AUDIO_EXTENSIONS.includes(ext)) return 'audio'
531+
if (THREE_D_EXTENSIONS.includes(ext)) return '3D'
522532

523533
return 'image'
524534
}
535+
536+
/**
537+
* @deprecated Use getMediaTypeFromFilename instead - returns plural form for legacy compatibility
538+
* @param filename The filename to analyze
539+
* @returns The media type in plural form: 'images', 'videos', 'audios', '3D'
540+
*/
541+
export function getMediaTypeFromFilenamePlural(filename: string): string {
542+
const type = getMediaTypeFromFilename(filename)
543+
switch (type) {
544+
case 'image':
545+
return 'images'
546+
case 'video':
547+
return 'videos'
548+
case 'audio':
549+
return 'audios'
550+
case '3D':
551+
return '3D'
552+
default:
553+
return 'images'
554+
}
555+
}
556+
557+
/**
558+
* @deprecated Use getMediaTypeFromFilename instead - kept for backward compatibility
559+
* @param filename The filename to analyze
560+
* @returns The media kind: 'image', 'video', 'audio', or '3D'
561+
*/
562+
export function getMediaKindFromFilename(
563+
filename: string
564+
): 'image' | 'video' | 'audio' | '3D' {
565+
return getMediaTypeFromFilename(filename)
566+
}

src/components/sidebar/tabs/AssetsSidebarTab.vue

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,23 +65,18 @@ import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
6565
import VirtualGrid from '@/components/common/VirtualGrid.vue'
6666
import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue'
6767
import ResultGallery from '@/components/sidebar/tabs/queue/ResultGallery.vue'
68-
import { useCloudMediaAssets } from '@/composables/useCloudMediaAssets'
69-
import { useInternalMediaAssets } from '@/composables/useInternalMediaAssets'
7068
import MediaAssetCard from '@/platform/assets/components/MediaAssetCard.vue'
69+
import { useMediaAssets } from '@/platform/assets/composables/useMediaAssets'
7170
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
72-
import { isCloud } from '@/platform/distribution/types'
7371
import { ResultItemImpl } from '@/stores/queueStore'
74-
import { getMediaTypeFromFilename } from '@/utils/formatUtil'
72+
import { getMediaTypeFromFilenamePlural } from '@/utils/formatUtil'
7573
7674
const activeTab = ref<'input' | 'output'>('input')
7775
const mediaAssets = ref<AssetItem[]>([])
7876
const selectedAsset = ref<AssetItem | null>(null)
7977
80-
// Use appropriate implementation based on environment
81-
const implementation = isCloud
82-
? useCloudMediaAssets()
83-
: useInternalMediaAssets()
84-
const { loading, error, fetchMediaList } = implementation
78+
// Use unified media assets implementation that handles cloud/internal automatically
79+
const { loading, error, fetchMediaList } = useMediaAssets()
8580
8681
const galleryActiveIndex = ref(-1)
8782
const galleryItems = computed(() => {
@@ -92,7 +87,7 @@ const galleryItems = computed(() => {
9287
subfolder: '',
9388
type: 'output',
9489
nodeId: '0',
95-
mediaType: getMediaTypeFromFilename(asset.name)
90+
mediaType: getMediaTypeFromFilenamePlural(asset.name)
9691
})
9792
9893
// Override the url getter to use asset.preview_url

src/platform/assets/components/MediaAssetCard.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ import CardBottom from '@/components/card/CardBottom.vue'
128128
import CardContainer from '@/components/card/CardContainer.vue'
129129
import CardTop from '@/components/card/CardTop.vue'
130130
import SquareChip from '@/components/chip/SquareChip.vue'
131-
import { formatDuration, getMediaKindFromFilename } from '@/utils/formatUtil'
131+
import { formatDuration, getMediaTypeFromFilename } from '@/utils/formatUtil'
132132
import { cn } from '@/utils/tailwindUtil'
133133
134134
import { useMediaAssetActions } from '../composables/useMediaAssetActions'
@@ -191,7 +191,7 @@ const assetType = computed(() => {
191191
192192
// Determine file type from extension
193193
const fileKind = computed((): MediaKind => {
194-
return getMediaKindFromFilename(asset?.name || '') as MediaKind
194+
return getMediaTypeFromFilename(asset?.name || '') as MediaKind
195195
})
196196
197197
// Adapt AssetItem to legacy AssetMeta format for existing components
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { Ref } from 'vue'
2+
3+
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
4+
5+
/**
6+
* Interface for media assets providers
7+
* Defines the common API for both cloud and internal file implementations
8+
*/
9+
export interface IAssetsProvider {
10+
/** Loading state indicator */
11+
loading: Ref<boolean>
12+
13+
/** Error state, null when no error */
14+
error: Ref<string | null>
15+
16+
/**
17+
* Fetch list of media assets from the specified directory
18+
* @param directory - 'input' or 'output'
19+
* @returns Promise resolving to array of AssetItem
20+
*/
21+
fetchMediaList: (directory: 'input' | 'output') => Promise<AssetItem[]>
22+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
2+
import { api } from '@/scripts/api'
3+
import type { TaskItemImpl } from '@/stores/queueStore'
4+
import { truncateFilename } from '@/utils/formatUtil'
5+
6+
/**
7+
* Maps a TaskItemImpl output to an AssetItem format
8+
* @param taskItem The task item containing execution data
9+
* @param output The output from the task
10+
* @param useDisplayName Whether to truncate the filename for display
11+
* @returns AssetItem formatted object
12+
*/
13+
export function mapTaskOutputToAssetItem(
14+
taskItem: TaskItemImpl,
15+
output: any,
16+
useDisplayName: boolean = false
17+
): AssetItem {
18+
const metadata: Record<string, any> = {
19+
promptId: taskItem.promptId,
20+
nodeId: output.nodeId,
21+
subfolder: output.subfolder
22+
}
23+
24+
// Add execution time if available
25+
if (taskItem.executionTimeInSeconds) {
26+
metadata.executionTimeInSeconds = taskItem.executionTimeInSeconds
27+
}
28+
29+
// Add format if available
30+
if (output.format) {
31+
metadata.format = output.format
32+
}
33+
34+
// Add workflow if available
35+
if (taskItem.workflow) {
36+
metadata.workflow = taskItem.workflow
37+
}
38+
39+
// Store original filename if using display name
40+
if (useDisplayName) {
41+
metadata.originalFilename = output.filename
42+
}
43+
44+
return {
45+
id: `${taskItem.promptId}-${output.nodeId}-${output.filename}`,
46+
name: useDisplayName
47+
? truncateFilename(output.filename, 20)
48+
: output.filename,
49+
size: 0, // Size not available from history API
50+
created_at: taskItem.executionStartTimestamp
51+
? new Date(taskItem.executionStartTimestamp).toISOString()
52+
: new Date().toISOString(),
53+
tags: ['output'],
54+
preview_url: output.url,
55+
user_metadata: metadata
56+
}
57+
}
58+
59+
/**
60+
* Maps input directory file to AssetItem format
61+
* @param filename The filename
62+
* @param index File index for unique ID
63+
* @param directory The directory type
64+
* @returns AssetItem formatted object
65+
*/
66+
export function mapInputFileToAssetItem(
67+
filename: string,
68+
index: number,
69+
directory: 'input' | 'output' = 'input'
70+
): AssetItem {
71+
return {
72+
id: `${directory}-${index}-${filename}`,
73+
name: filename,
74+
size: 0,
75+
created_at: new Date().toISOString(),
76+
tags: [directory],
77+
preview_url: api.apiURL(
78+
`/view?filename=${encodeURIComponent(filename)}&type=${directory}`
79+
)
80+
}
81+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { isCloud } from '@/platform/distribution/types'
2+
3+
import type { IAssetsProvider } from './IAssetsProvider'
4+
import { useAssetsApi } from './useAssetsApi'
5+
import { useInternalFilesApi } from './useInternalFilesApi'
6+
7+
/**
8+
* Factory function that returns the appropriate media assets implementation
9+
* based on the current distribution (cloud vs internal)
10+
* @returns IAssetsProvider implementation
11+
*/
12+
export function useMediaAssets(): IAssetsProvider {
13+
return isCloud ? useAssetsApi() : useInternalFilesApi()
14+
}
15+
16+
// Re-export the interface for consumers
17+
export type { IAssetsProvider } from './IAssetsProvider'

src/composables/useCloudMediaAssets.ts renamed to src/platform/assets/composables/useMediaAssets/useAssetsApi.ts

Lines changed: 9 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import type { HistoryTaskItem } from '@/schemas/apiSchema'
66
import { api } from '@/scripts/api'
77
import { TaskItemImpl } from '@/stores/queueStore'
88

9+
import { mapTaskOutputToAssetItem } from './assetMappers'
10+
911
/**
1012
* Composable for fetching media assets from cloud environment
1113
* Includes execution time from history API
1214
*/
13-
export function useCloudMediaAssets() {
15+
export function useAssetsApi() {
1416
const loading = ref(false)
1517
const error = ref<string | null>(null)
1618

@@ -53,62 +55,16 @@ export function useCloudMediaAssets() {
5355

5456
// Only process completed tasks
5557
if (taskItem.displayStatus === 'Completed' && taskItem.outputs) {
56-
// Get execution time
57-
const executionTimeInSeconds = taskItem.executionTimeInSeconds
58-
5958
// Process each output
6059
taskItem.flatOutputs.forEach((output) => {
6160
// Only include output type files (not temp previews)
6261
if (output.type === 'output' && output.supportsPreview) {
63-
// Truncate filename if longer than 15 characters
64-
let displayName = output.filename
65-
if (output.filename.length > 20) {
66-
// Get file extension
67-
const lastDotIndex = output.filename.lastIndexOf('.')
68-
const nameWithoutExt =
69-
lastDotIndex > -1
70-
? output.filename.substring(0, lastDotIndex)
71-
: output.filename
72-
const extension =
73-
lastDotIndex > -1
74-
? output.filename.substring(lastDotIndex)
75-
: ''
76-
77-
// If name without extension is still long, truncate it
78-
if (nameWithoutExt.length > 10) {
79-
displayName =
80-
nameWithoutExt.substring(0, 10) +
81-
'...' +
82-
nameWithoutExt.substring(nameWithoutExt.length - 10) +
83-
extension
84-
}
85-
}
86-
87-
assetItems.push({
88-
id: `${taskItem.promptId}-${output.nodeId}-${output.filename}`,
89-
name: displayName,
90-
size: 0, // We don't have size info from history
91-
created_at: taskItem.executionStartTimestamp
92-
? new Date(taskItem.executionStartTimestamp).toISOString()
93-
: new Date().toISOString(),
94-
tags: ['output'],
95-
preview_url: output.url,
96-
user_metadata: {
97-
originalFilename: output.filename, // Store original filename
98-
promptId: taskItem.promptId,
99-
nodeId: output.nodeId,
100-
subfolder: output.subfolder,
101-
...(executionTimeInSeconds && {
102-
executionTimeInSeconds
103-
}),
104-
...(output.format && {
105-
format: output.format
106-
}),
107-
...(taskItem.workflow && {
108-
workflow: taskItem.workflow
109-
})
110-
}
111-
})
62+
const assetItem = mapTaskOutputToAssetItem(
63+
taskItem,
64+
output,
65+
true // Use display name for cloud
66+
)
67+
assetItems.push(assetItem)
11268
}
11369
})
11470
}

0 commit comments

Comments
 (0)