mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-22 07:44:11 +00:00
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:
16
src/composables/sidebarTabs/useAssetsSidebarTab.ts
Normal file
16
src/composables/sidebarTabs/useAssetsSidebarTab.ts
Normal 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'
|
||||
}
|
||||
}
|
||||
133
src/composables/useCloudMediaAssets.ts
Normal file
133
src/composables/useCloudMediaAssets.ts
Normal 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
|
||||
}
|
||||
}
|
||||
129
src/composables/useInternalMediaAssets.ts
Normal file
129
src/composables/useInternalMediaAssets.ts
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user