feat: Add Media Assets sidebar tab for file management

- Implement new sidebar tab for managing imported/generated files
- Add separate composables for internal and cloud environments
- Display execution time from history API on generated outputs
- Support gallery view with keyboard navigation
- Auto-truncate long filenames in cloud environment
- Add utility functions for media type detection
- Enable feature only in development mode

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jin Yi
2025-10-18 00:05:54 +09:00
parent fd2a52500c
commit 2bb54650b4
12 changed files with 720 additions and 157 deletions

View File

@@ -0,0 +1,16 @@
import { markRaw } from 'vue'
import AssetsSidebarTab from '@/components/sidebar/tabs/AssetsSidebarTab.vue'
import type { SidebarTabExtension } from '@/types/extensionTypes'
export const useAssetsSidebarTab = (): SidebarTabExtension => {
return {
id: 'assets',
icon: 'icon-[comfy--image-ai-edit]',
title: 'sideToolbar.assets',
tooltip: 'sideToolbar.assets',
label: 'sideToolbar.labels.assets',
component: markRaw(AssetsSidebarTab),
type: 'vue'
}
}

View File

@@ -0,0 +1,133 @@
import { ref } from 'vue'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { assetService } from '@/platform/assets/services/assetService'
import type { HistoryTaskItem } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { TaskItemImpl } from '@/stores/queueStore'
/**
* Composable for fetching media assets from cloud environment
* Includes execution time from history API
*/
export function useCloudMediaAssets() {
const loading = ref(false)
const error = ref<string | null>(null)
/**
* Fetch list of assets from cloud with execution time
* @param directory - 'input' or 'output'
* @returns Array of AssetItem with execution time in user_metadata
*/
const fetchMediaList = async (
directory: 'input' | 'output'
): Promise<AssetItem[]> => {
loading.value = true
error.value = null
try {
// For input directory, just return assets without history
if (directory === 'input') {
const assets = await assetService.getAssetsByTag(directory)
return assets
}
// For output directory, fetch history data and convert to AssetItem format
const historyResponse = await api.getHistory(200)
if (!historyResponse?.History) {
return []
}
// Convert history items to AssetItem format
const assetItems: AssetItem[] = []
historyResponse.History.forEach((historyItem: HistoryTaskItem) => {
// Create TaskItemImpl to use existing logic
const taskItem = new TaskItemImpl(
historyItem.taskType,
historyItem.prompt,
historyItem.status,
historyItem.outputs
)
// 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
})
}
})
}
})
}
})
return assetItems
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error'
console.error(`Error fetching ${directory} cloud assets:`, errorMessage)
error.value = errorMessage
return []
} finally {
loading.value = false
}
}
return {
loading,
error,
fetchMediaList
}
}

View File

@@ -0,0 +1,129 @@
import { ref } from 'vue'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import type { HistoryTaskItem } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { TaskItemImpl } from '@/stores/queueStore'
/**
* Composable for fetching media assets from local environment
* Uses the same logic as QueueSidebarTab for history processing
*/
export function useInternalMediaAssets() {
const loading = ref(false)
const error = ref<string | null>(null)
/**
* Fetch list of files from input or output directory with execution time
* @param directory - 'input' or 'output'
* @returns Array of AssetItem with execution time in user_metadata
*/
const fetchMediaList = async (
directory: 'input' | 'output'
): Promise<AssetItem[]> => {
loading.value = true
error.value = null
try {
// For input directory, fetch files without history
if (directory === 'input') {
const response = await fetch(api.internalURL(`/files/${directory}`), {
headers: {
'Comfy-User': api.user
}
})
if (!response.ok) {
throw new Error(`Failed to fetch ${directory} files`)
}
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}`
)
}))
}
// For output directory, use history data like QueueSidebarTab
const historyResponse = await api.getHistory(200)
if (!historyResponse?.History) {
return []
}
const assetItems: AssetItem[] = []
// Process history items using TaskItemImpl like QueueSidebarTab
historyResponse.History.forEach((historyItem: HistoryTaskItem) => {
// Create TaskItemImpl to use the same logic as QueueSidebarTab
const taskItem = new TaskItemImpl(
'History',
historyItem.prompt,
historyItem.status,
historyItem.outputs
)
// 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
})
}
})
}
})
}
})
// Sort by creation date (newest first)
return assetItems.sort(
(a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
)
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error'
console.error(`Error fetching ${directory} assets:`, errorMessage)
error.value = errorMessage
return []
} finally {
loading.value = false
}
}
return {
loading,
error,
fetchMediaList
}
}