mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 22:37:32 +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:
5
packages/design-system/src/icons/image-ai-edit.svg
Normal file
5
packages/design-system/src/icons/image-ai-edit.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 9.99996L11.9427 7.94263C11.6926 7.69267 11.3536 7.55225 11 7.55225C10.6464 7.55225 10.3074 7.69267 10.0573 7.94263L9 9M8 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5.51377 12.671L4.77612 14.3921C4.67222 14.6346 4.32853 14.6346 4.22463 14.3921L3.48699 12.671C3.45664 12.6002 3.40022 12.5437 3.32942 12.5134L1.60825 11.7757C1.36581 11.6718 1.36581 11.3282 1.60825 11.2243L3.32942 10.4866C3.40022 10.4563 3.45664 10.3998 3.48699 10.329L4.22463 8.60787C4.32853 8.36544 4.67222 8.36544 4.77612 8.60787L5.51377 10.329C5.54411 10.3998 5.60053 10.4563 5.67134 10.4866L7.39251 11.2243C7.63494 11.3282 7.63494 11.6718 7.39251 11.7757L5.67134 12.5134C5.60053 12.5437 5.54411 12.6002 5.51377 12.671Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M5 5H5.0001" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -474,3 +474,51 @@ export function formatDuration(milliseconds: number): string {
|
||||
|
||||
return parts.join(' ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the media type from a filename's extension
|
||||
* @param filename The filename to analyze
|
||||
* @returns The media type: 'images', 'videos', 'audios', '3D' for gallery compatibility
|
||||
*/
|
||||
export function getMediaTypeFromFilename(filename: string): string {
|
||||
if (!filename) return 'images'
|
||||
const ext = filename.split('.').pop()?.toLowerCase()
|
||||
if (!ext) return 'images'
|
||||
|
||||
const imageExts = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp']
|
||||
const videoExts = ['mp4', 'webm', 'mov', 'avi']
|
||||
const audioExts = ['mp3', 'wav', 'ogg', 'flac']
|
||||
const threeDExts = ['obj', 'fbx', 'gltf', 'glb']
|
||||
|
||||
if (imageExts.includes(ext)) return 'images'
|
||||
if (videoExts.includes(ext)) return 'videos'
|
||||
if (audioExts.includes(ext)) return 'audios'
|
||||
if (threeDExts.includes(ext)) return '3D'
|
||||
|
||||
return 'images'
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the media kind from a filename's extension
|
||||
* @param filename The filename to analyze
|
||||
* @returns The media kind: 'image', 'video', 'audio', or '3D'
|
||||
*/
|
||||
export function getMediaKindFromFilename(
|
||||
filename: string
|
||||
): 'image' | 'video' | 'audio' | '3D' {
|
||||
if (!filename) return 'image'
|
||||
const ext = filename.split('.').pop()?.toLowerCase()
|
||||
if (!ext) return 'image'
|
||||
|
||||
const imageExts = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp']
|
||||
const videoExts = ['mp4', 'webm', 'mov', 'avi']
|
||||
const audioExts = ['mp3', 'wav', 'ogg', 'flac']
|
||||
const threeDExts = ['obj', 'fbx', 'gltf', 'glb']
|
||||
|
||||
if (imageExts.includes(ext)) return 'image'
|
||||
if (videoExts.includes(ext)) return 'video'
|
||||
if (audioExts.includes(ext)) return 'audio'
|
||||
if (threeDExts.includes(ext)) return '3D'
|
||||
|
||||
return 'image'
|
||||
}
|
||||
|
||||
150
src/components/sidebar/tabs/AssetsSidebarTab.vue
Normal file
150
src/components/sidebar/tabs/AssetsSidebarTab.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<SidebarTabTemplate :title="$t('sideToolbar.mediaAssets')">
|
||||
<template #header>
|
||||
<Tabs v-model:value="activeTab" class="w-full">
|
||||
<TabList class="border-b border-neutral-300">
|
||||
<Tab value="input">{{ $t('sideToolbar.labels.imported') }}</Tab>
|
||||
<Tab value="output">{{ $t('sideToolbar.labels.generated') }}</Tab>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</template>
|
||||
<template #body>
|
||||
<VirtualGrid
|
||||
v-if="mediaAssets.length"
|
||||
:items="mediaAssetsWithKey"
|
||||
:grid-style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
|
||||
padding: '0.5rem',
|
||||
gap: '0.5rem'
|
||||
}"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<MediaAssetCard
|
||||
:asset="item"
|
||||
:selected="selectedAsset?.id === item.id"
|
||||
@click="handleAssetSelect(item)"
|
||||
@zoom="handleZoomClick(item)"
|
||||
/>
|
||||
</template>
|
||||
</VirtualGrid>
|
||||
<div v-else-if="loading">
|
||||
<ProgressSpinner
|
||||
style="width: 50px; left: 50%; transform: translateX(-50%)"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<NoResultsPlaceholder
|
||||
icon="pi pi-info-circle"
|
||||
:title="
|
||||
$t(
|
||||
activeTab === 'input'
|
||||
? 'sideToolbar.noImportedFiles'
|
||||
: 'sideToolbar.noGeneratedFiles'
|
||||
)
|
||||
"
|
||||
:message="$t('sideToolbar.noFilesFoundMessage')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</SidebarTabTemplate>
|
||||
<ResultGallery
|
||||
v-model:active-index="galleryActiveIndex"
|
||||
:all-gallery-items="galleryItems"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import Tab from 'primevue/tab'
|
||||
import TabList from 'primevue/tablist'
|
||||
import Tabs from 'primevue/tabs'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
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 type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { ResultItemImpl } from '@/stores/queueStore'
|
||||
import { getMediaTypeFromFilename } 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
|
||||
|
||||
const galleryActiveIndex = ref(-1)
|
||||
const galleryItems = computed(() => {
|
||||
// Convert AssetItems to ResultItemImpl format for gallery
|
||||
return mediaAssets.value.map((asset) => {
|
||||
const resultItem = new ResultItemImpl({
|
||||
filename: asset.name,
|
||||
subfolder: '',
|
||||
type: 'output',
|
||||
nodeId: '0',
|
||||
mediaType: getMediaTypeFromFilename(asset.name)
|
||||
})
|
||||
|
||||
// Override the url getter to use asset.preview_url
|
||||
Object.defineProperty(resultItem, 'url', {
|
||||
get() {
|
||||
return asset.preview_url || ''
|
||||
},
|
||||
configurable: true
|
||||
})
|
||||
|
||||
return resultItem
|
||||
})
|
||||
})
|
||||
|
||||
// Add key property for VirtualGrid
|
||||
const mediaAssetsWithKey = computed(() => {
|
||||
return mediaAssets.value.map((asset) => ({
|
||||
...asset,
|
||||
key: asset.id
|
||||
}))
|
||||
})
|
||||
|
||||
const refreshAssets = async () => {
|
||||
const files = await fetchMediaList(activeTab.value)
|
||||
mediaAssets.value = files
|
||||
if (error.value) {
|
||||
console.error('Failed to refresh assets:', error.value)
|
||||
}
|
||||
}
|
||||
|
||||
watch(activeTab, () => {
|
||||
void refreshAssets()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void refreshAssets()
|
||||
})
|
||||
|
||||
const handleAssetSelect = (asset: AssetItem) => {
|
||||
// Toggle selection
|
||||
if (selectedAsset.value?.id === asset.id) {
|
||||
selectedAsset.value = null
|
||||
} else {
|
||||
selectedAsset.value = asset
|
||||
}
|
||||
}
|
||||
|
||||
const handleZoomClick = (asset: AssetItem) => {
|
||||
// Find the index of the clicked asset
|
||||
const index = mediaAssets.value.findIndex((a) => a.id === asset.id)
|
||||
if (index !== -1) {
|
||||
galleryActiveIndex.value = index
|
||||
}
|
||||
}
|
||||
</script>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -581,13 +581,24 @@
|
||||
"nodeLibrary": "Node Library",
|
||||
"workflows": "Workflows",
|
||||
"templates": "Templates",
|
||||
"assets": "Assets",
|
||||
"mediaAssets": "Media Assets",
|
||||
"labels": {
|
||||
"queue": "Queue",
|
||||
"nodes": "Nodes",
|
||||
"models": "Models",
|
||||
"workflows": "Workflows",
|
||||
"templates": "Templates"
|
||||
"templates": "Templates",
|
||||
"console": "Console",
|
||||
"menu": "Menu",
|
||||
"assets": "Assets",
|
||||
"imported": "Imported",
|
||||
"generated": "Generated"
|
||||
},
|
||||
"noFilesFound": "No files found",
|
||||
"noImportedFiles": "No imported files found",
|
||||
"noGeneratedFiles": "No generated files found",
|
||||
"noFilesFoundMessage": "Upload files or generate content to see them here",
|
||||
"browseTemplates": "Browse example templates",
|
||||
"openWorkflow": "Open workflow in local file system",
|
||||
"newBlankWorkflow": "Create a new blank workflow",
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import ResultGallery from '@/components/sidebar/tabs/queue/ResultGallery.vue'
|
||||
|
||||
import { useMediaAssetGalleryStore } from '../composables/useMediaAssetGalleryStore'
|
||||
import type { AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
import type { AssetItem } from '../schemas/assetSchema'
|
||||
import MediaAssetCard from './MediaAssetCard.vue'
|
||||
|
||||
const meta: Meta<typeof MediaAssetCard> = {
|
||||
@@ -28,10 +28,6 @@ const meta: Meta<typeof MediaAssetCard> = {
|
||||
})
|
||||
],
|
||||
argTypes: {
|
||||
context: {
|
||||
control: 'select',
|
||||
options: ['input', 'output']
|
||||
},
|
||||
loading: {
|
||||
control: 'boolean'
|
||||
}
|
||||
@@ -53,19 +49,20 @@ const SAMPLE_MEDIA = {
|
||||
audio: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'
|
||||
}
|
||||
|
||||
const sampleAsset: AssetMeta = {
|
||||
const sampleAsset: AssetItem = {
|
||||
id: 'asset-1',
|
||||
name: 'sample-image.png',
|
||||
kind: 'image',
|
||||
duration: 3345,
|
||||
size: 2048576,
|
||||
created_at: Date.now().toString(),
|
||||
src: SAMPLE_MEDIA.image1,
|
||||
dimensions: {
|
||||
width: 1920,
|
||||
height: 1080
|
||||
},
|
||||
tags: []
|
||||
created_at: new Date().toISOString(),
|
||||
preview_url: SAMPLE_MEDIA.image1,
|
||||
tags: ['input'],
|
||||
user_metadata: {
|
||||
duration: 3345,
|
||||
dimensions: {
|
||||
width: 1920,
|
||||
height: 1080
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ImageAsset: Story = {
|
||||
@@ -75,7 +72,6 @@ export const ImageAsset: Story = {
|
||||
})
|
||||
],
|
||||
args: {
|
||||
context: { type: 'output', outputCount: 3 },
|
||||
asset: sampleAsset,
|
||||
loading: false
|
||||
}
|
||||
@@ -88,19 +84,18 @@ export const VideoAsset: Story = {
|
||||
})
|
||||
],
|
||||
args: {
|
||||
context: { type: 'input' },
|
||||
asset: {
|
||||
...sampleAsset,
|
||||
id: 'asset-2',
|
||||
name: 'Big_Buck_Bunny.mp4',
|
||||
kind: 'video',
|
||||
size: 10485760,
|
||||
duration: 13425,
|
||||
preview_url: SAMPLE_MEDIA.videoThumbnail, // Poster image
|
||||
src: SAMPLE_MEDIA.video, // Actual video file
|
||||
dimensions: {
|
||||
width: 1280,
|
||||
height: 720
|
||||
preview_url: SAMPLE_MEDIA.videoThumbnail,
|
||||
user_metadata: {
|
||||
duration: 13425,
|
||||
dimensions: {
|
||||
width: 1280,
|
||||
height: 720
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,16 +108,15 @@ export const Model3DAsset: Story = {
|
||||
})
|
||||
],
|
||||
args: {
|
||||
context: { type: 'input' },
|
||||
asset: {
|
||||
...sampleAsset,
|
||||
id: 'asset-3',
|
||||
name: 'Asset-3d-model.glb',
|
||||
kind: '3D',
|
||||
size: 7340032,
|
||||
src: '',
|
||||
dimensions: undefined,
|
||||
duration: 18023
|
||||
preview_url: '',
|
||||
user_metadata: {
|
||||
duration: 18023
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,16 +128,15 @@ export const AudioAsset: Story = {
|
||||
})
|
||||
],
|
||||
args: {
|
||||
context: { type: 'input' },
|
||||
asset: {
|
||||
...sampleAsset,
|
||||
id: 'asset-3',
|
||||
id: 'asset-4',
|
||||
name: 'SoundHelix-Song.mp3',
|
||||
kind: 'audio',
|
||||
size: 5242880,
|
||||
src: SAMPLE_MEDIA.audio,
|
||||
dimensions: undefined,
|
||||
duration: 23180
|
||||
preview_url: SAMPLE_MEDIA.audio,
|
||||
user_metadata: {
|
||||
duration: 23180
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,7 +148,6 @@ export const LoadingState: Story = {
|
||||
})
|
||||
],
|
||||
args: {
|
||||
context: { type: 'input' },
|
||||
asset: sampleAsset,
|
||||
loading: true
|
||||
}
|
||||
@@ -168,7 +160,6 @@ export const LongFileName: Story = {
|
||||
})
|
||||
],
|
||||
args: {
|
||||
context: { type: 'input' },
|
||||
asset: {
|
||||
...sampleAsset,
|
||||
name: 'very-long-file-name-that-should-be-truncated-in-the-ui-to-prevent-overflow.png'
|
||||
@@ -183,7 +174,6 @@ export const SelectedState: Story = {
|
||||
})
|
||||
],
|
||||
args: {
|
||||
context: { type: 'output', outputCount: 2 },
|
||||
asset: sampleAsset,
|
||||
selected: true
|
||||
}
|
||||
@@ -196,21 +186,20 @@ export const WebMVideo: Story = {
|
||||
})
|
||||
],
|
||||
args: {
|
||||
context: { type: 'input' },
|
||||
asset: {
|
||||
id: 'asset-webm',
|
||||
name: 'animated-clip.webm',
|
||||
kind: 'video',
|
||||
size: 3145728,
|
||||
created_at: Date.now().toString(),
|
||||
preview_url: SAMPLE_MEDIA.image1, // Poster image
|
||||
src: 'https://www.w3schools.com/html/movie.mp4', // Actual video
|
||||
duration: 620,
|
||||
dimensions: {
|
||||
width: 640,
|
||||
height: 360
|
||||
},
|
||||
tags: []
|
||||
created_at: new Date().toISOString(),
|
||||
preview_url: SAMPLE_MEDIA.image1,
|
||||
tags: ['input'],
|
||||
user_metadata: {
|
||||
duration: 620,
|
||||
dimensions: {
|
||||
width: 640,
|
||||
height: 360
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,20 +211,20 @@ export const GifAnimation: Story = {
|
||||
})
|
||||
],
|
||||
args: {
|
||||
context: { type: 'input' },
|
||||
asset: {
|
||||
id: 'asset-gif',
|
||||
name: 'animation.gif',
|
||||
kind: 'image',
|
||||
size: 1572864,
|
||||
duration: 1345,
|
||||
created_at: Date.now().toString(),
|
||||
src: 'https://media.giphy.com/media/3o7aCTPPm4OHfRLSH6/giphy.gif',
|
||||
dimensions: {
|
||||
width: 480,
|
||||
height: 270
|
||||
},
|
||||
tags: []
|
||||
created_at: new Date().toISOString(),
|
||||
preview_url: 'https://media.giphy.com/media/3o7aCTPPm4OHfRLSH6/giphy.gif',
|
||||
tags: ['input'],
|
||||
user_metadata: {
|
||||
duration: 1345,
|
||||
dimensions: {
|
||||
width: 480,
|
||||
height: 270
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,83 +233,89 @@ export const GridLayout: Story = {
|
||||
render: () => ({
|
||||
components: { MediaAssetCard },
|
||||
setup() {
|
||||
const assets: AssetMeta[] = [
|
||||
const assets: AssetItem[] = [
|
||||
{
|
||||
id: 'grid-1',
|
||||
name: 'image-file.jpg',
|
||||
kind: 'image',
|
||||
size: 2097152,
|
||||
duration: 4500,
|
||||
created_at: Date.now().toString(),
|
||||
src: SAMPLE_MEDIA.image1,
|
||||
dimensions: { width: 1920, height: 1080 },
|
||||
tags: []
|
||||
created_at: new Date().toISOString(),
|
||||
preview_url: SAMPLE_MEDIA.image1,
|
||||
tags: ['input'],
|
||||
user_metadata: {
|
||||
duration: 4500,
|
||||
dimensions: { width: 1920, height: 1080 }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'grid-2',
|
||||
name: 'image-file.jpg',
|
||||
kind: 'image',
|
||||
size: 2097152,
|
||||
duration: 4500,
|
||||
created_at: Date.now().toString(),
|
||||
src: SAMPLE_MEDIA.image2,
|
||||
dimensions: { width: 1920, height: 1080 },
|
||||
tags: []
|
||||
created_at: new Date().toISOString(),
|
||||
preview_url: SAMPLE_MEDIA.image2,
|
||||
tags: ['input'],
|
||||
user_metadata: {
|
||||
duration: 4500,
|
||||
dimensions: { width: 1920, height: 1080 }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'grid-3',
|
||||
name: 'video-file.mp4',
|
||||
kind: 'video',
|
||||
size: 10485760,
|
||||
duration: 13425,
|
||||
created_at: Date.now().toString(),
|
||||
preview_url: SAMPLE_MEDIA.videoThumbnail, // Poster image
|
||||
src: SAMPLE_MEDIA.video, // Actual video
|
||||
dimensions: { width: 1280, height: 720 },
|
||||
tags: []
|
||||
created_at: new Date().toISOString(),
|
||||
preview_url: SAMPLE_MEDIA.videoThumbnail,
|
||||
tags: ['input'],
|
||||
user_metadata: {
|
||||
duration: 13425,
|
||||
dimensions: { width: 1280, height: 720 }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'grid-4',
|
||||
name: 'audio-file.mp3',
|
||||
kind: 'audio',
|
||||
size: 5242880,
|
||||
duration: 180,
|
||||
created_at: Date.now().toString(),
|
||||
src: SAMPLE_MEDIA.audio,
|
||||
tags: []
|
||||
created_at: new Date().toISOString(),
|
||||
preview_url: SAMPLE_MEDIA.audio,
|
||||
tags: ['input'],
|
||||
user_metadata: {
|
||||
duration: 180
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'grid-5',
|
||||
name: 'animation.gif',
|
||||
kind: 'image',
|
||||
size: 3145728,
|
||||
duration: 1345,
|
||||
created_at: Date.now().toString(),
|
||||
src: 'https://media.giphy.com/media/l0HlNaQ6gWfllcjDO/giphy.gif',
|
||||
dimensions: { width: 480, height: 360 },
|
||||
tags: []
|
||||
created_at: new Date().toISOString(),
|
||||
preview_url:
|
||||
'https://media.giphy.com/media/l0HlNaQ6gWfllcjDO/giphy.gif',
|
||||
tags: ['input'],
|
||||
user_metadata: {
|
||||
duration: 1345,
|
||||
dimensions: { width: 480, height: 360 }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'grid-6',
|
||||
name: 'Asset-3d-model.glb',
|
||||
kind: '3D',
|
||||
size: 7340032,
|
||||
src: '',
|
||||
dimensions: undefined,
|
||||
duration: 18023,
|
||||
created_at: Date.now().toString(),
|
||||
tags: []
|
||||
preview_url: '',
|
||||
created_at: new Date().toISOString(),
|
||||
tags: ['input'],
|
||||
user_metadata: {
|
||||
duration: 18023
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'grid-7',
|
||||
name: 'image-file.jpg',
|
||||
kind: 'image',
|
||||
size: 2097152,
|
||||
duration: 4500,
|
||||
created_at: Date.now().toString(),
|
||||
src: SAMPLE_MEDIA.image3,
|
||||
dimensions: { width: 1920, height: 1080 },
|
||||
tags: []
|
||||
created_at: new Date().toISOString(),
|
||||
preview_url: SAMPLE_MEDIA.image3,
|
||||
tags: ['input'],
|
||||
user_metadata: {
|
||||
duration: 4500,
|
||||
dimensions: { width: 1920, height: 1080 }
|
||||
}
|
||||
}
|
||||
]
|
||||
return { assets }
|
||||
@@ -330,7 +325,6 @@ export const GridLayout: Story = {
|
||||
<MediaAssetCard
|
||||
v-for="asset in assets"
|
||||
:key="asset.id"
|
||||
:context="{ type: Math.random() > 0.5 ? 'input' : 'output', outputCount: Math.floor(Math.random() * 5) }"
|
||||
:asset="asset"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
<CardContainer
|
||||
ref="cardContainerRef"
|
||||
role="button"
|
||||
:aria-label="
|
||||
asset ? `${asset.name} - ${asset.kind} asset` : 'Loading asset'
|
||||
"
|
||||
:aria-label="asset ? `${asset.name} - ${fileKind} asset` : 'Loading asset'"
|
||||
:tabindex="loading ? -1 : 0"
|
||||
size="mini"
|
||||
variant="ghost"
|
||||
@@ -28,16 +26,17 @@
|
||||
</template>
|
||||
|
||||
<!-- Content based on asset type -->
|
||||
<template v-else-if="asset">
|
||||
<template v-else-if="asset && adaptedAsset">
|
||||
<component
|
||||
:is="getTopComponent(asset.kind)"
|
||||
:asset="asset"
|
||||
:context="context"
|
||||
:is="getTopComponent(fileKind)"
|
||||
:asset="adaptedAsset"
|
||||
:context="{ type: assetType }"
|
||||
@view="handleZoomClick"
|
||||
@download="actions.downloadAsset(asset!.id)"
|
||||
@play="actions.playAsset(asset!.id)"
|
||||
@download="actions.downloadAsset(asset.id)"
|
||||
@play="actions.playAsset(asset.id)"
|
||||
@video-playing-state-changed="isVideoPlaying = $event"
|
||||
@video-controls-changed="showVideoControls = $event"
|
||||
@image-loaded="handleImageLoaded"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -79,7 +78,7 @@
|
||||
<IconTextButton
|
||||
type="secondary"
|
||||
size="sm"
|
||||
:label="context?.outputCount?.toString() ?? '0'"
|
||||
:label="'0'"
|
||||
@click.stop="actions.openMoreOutputs(asset?.id || '')"
|
||||
@mouseenter="handleOverlayMouseEnter"
|
||||
@mouseleave="handleOverlayMouseLeave"
|
||||
@@ -107,11 +106,11 @@
|
||||
</template>
|
||||
|
||||
<!-- Content based on asset type -->
|
||||
<template v-else-if="asset">
|
||||
<template v-else-if="asset && adaptedAsset">
|
||||
<component
|
||||
:is="getBottomComponent(asset.kind)"
|
||||
:asset="asset"
|
||||
:context="context"
|
||||
:is="getBottomComponent(fileKind)"
|
||||
:asset="adaptedAsset"
|
||||
:context="{ type: assetType }"
|
||||
/>
|
||||
</template>
|
||||
</CardBottom>
|
||||
@@ -129,16 +128,12 @@ 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 } from '@/utils/formatUtil'
|
||||
import { formatDuration, getMediaKindFromFilename } from '@/utils/formatUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import { useMediaAssetActions } from '../composables/useMediaAssetActions'
|
||||
import { useMediaAssetGalleryStore } from '../composables/useMediaAssetGalleryStore'
|
||||
import type {
|
||||
AssetContext,
|
||||
AssetMeta,
|
||||
MediaKind
|
||||
} from '../schemas/mediaAssetSchema'
|
||||
import type { AssetItem } from '../schemas/assetSchema'
|
||||
import type { MediaKind } from '../schemas/mediaAssetSchema'
|
||||
import { MediaAssetKey } from '../schemas/mediaAssetSchema'
|
||||
import MediaAssetActions from './MediaAssetActions.vue'
|
||||
|
||||
@@ -165,13 +160,16 @@ function getBottomComponent(kind: MediaKind) {
|
||||
return mediaComponents.bottom[kind] || mediaComponents.bottom.image
|
||||
}
|
||||
|
||||
const { context, asset, loading, selected } = defineProps<{
|
||||
context: AssetContext
|
||||
asset?: AssetMeta
|
||||
const { asset, loading, selected } = defineProps<{
|
||||
asset?: AssetItem
|
||||
loading?: boolean
|
||||
selected?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
zoom: [asset: AssetItem]
|
||||
}>()
|
||||
|
||||
const cardContainerRef = ref<HTMLElement>()
|
||||
|
||||
const isVideoPlaying = ref(false)
|
||||
@@ -179,14 +177,48 @@ const isMenuOpen = ref(false)
|
||||
const showVideoControls = ref(false)
|
||||
const isOverlayHovered = ref(false)
|
||||
|
||||
// Store actual image dimensions
|
||||
const imageDimensions = ref<{ width: number; height: number } | undefined>()
|
||||
|
||||
const isHovered = useElementHover(cardContainerRef)
|
||||
|
||||
const actions = useMediaAssetActions()
|
||||
const galleryStore = useMediaAssetGalleryStore()
|
||||
|
||||
// Get asset type from tags[0]
|
||||
const assetType = computed(() => {
|
||||
return (asset?.tags?.[0] as 'input' | 'output') || 'input'
|
||||
})
|
||||
|
||||
// Determine file type from extension
|
||||
const fileKind = computed((): MediaKind => {
|
||||
return getMediaKindFromFilename(asset?.name || '') as MediaKind
|
||||
})
|
||||
|
||||
// Adapt AssetItem to legacy AssetMeta format for existing components
|
||||
const adaptedAsset = computed(() => {
|
||||
if (!asset) return undefined
|
||||
return {
|
||||
id: asset.id,
|
||||
name: asset.name,
|
||||
kind: fileKind.value,
|
||||
src: asset.preview_url || '',
|
||||
size: asset.size,
|
||||
tags: asset.tags || [],
|
||||
created_at: asset.created_at,
|
||||
duration: asset.user_metadata?.duration
|
||||
? Number(asset.user_metadata.duration)
|
||||
: undefined,
|
||||
dimensions:
|
||||
imageDimensions.value ||
|
||||
(asset.user_metadata?.dimensions as
|
||||
| { width: number; height: number }
|
||||
| undefined)
|
||||
}
|
||||
})
|
||||
|
||||
provide(MediaAssetKey, {
|
||||
asset: toRef(() => asset),
|
||||
context: toRef(() => context),
|
||||
asset: toRef(() => adaptedAsset.value),
|
||||
context: toRef(() => ({ type: assetType.value })),
|
||||
isVideoPlaying,
|
||||
showVideoControls
|
||||
})
|
||||
@@ -201,8 +233,16 @@ const containerClasses = computed(() =>
|
||||
)
|
||||
|
||||
const formattedDuration = computed(() => {
|
||||
if (!asset?.duration) return ''
|
||||
return formatDuration(asset.duration)
|
||||
// Check for execution time first (from history API)
|
||||
const executionTime = asset?.user_metadata?.executionTimeInSeconds
|
||||
if (executionTime !== undefined && executionTime !== null) {
|
||||
return `${Number(executionTime).toFixed(2)}s`
|
||||
}
|
||||
|
||||
// Fall back to duration for media files
|
||||
const duration = asset?.user_metadata?.duration
|
||||
if (!duration) return ''
|
||||
return formatDuration(Number(duration))
|
||||
})
|
||||
|
||||
const fileFormat = computed(() => {
|
||||
@@ -212,10 +252,10 @@ const fileFormat = computed(() => {
|
||||
})
|
||||
|
||||
const durationChipClasses = computed(() => {
|
||||
if (asset?.kind === 'audio') {
|
||||
if (fileKind.value === 'audio') {
|
||||
return '-translate-y-11'
|
||||
}
|
||||
if (asset?.kind === 'video' && showVideoControls.value) {
|
||||
if (fileKind.value === 'video' && showVideoControls.value) {
|
||||
return '-translate-y-16'
|
||||
}
|
||||
return ''
|
||||
@@ -229,36 +269,35 @@ const showHoverActions = computed(
|
||||
() => !loading && !!asset && isCardOrOverlayHovered.value
|
||||
)
|
||||
|
||||
const showActionsOverlay = computed(
|
||||
() =>
|
||||
showHoverActions.value &&
|
||||
(!isVideoPlaying.value || isCardOrOverlayHovered.value)
|
||||
)
|
||||
const showActionsOverlay = false
|
||||
// const showActionsOverlay = computed(
|
||||
// () =>
|
||||
// showHoverActions.value &&
|
||||
// (!isVideoPlaying.value || isCardOrOverlayHovered.value)
|
||||
// )
|
||||
|
||||
const showZoomOverlay = computed(
|
||||
() =>
|
||||
showHoverActions.value &&
|
||||
asset?.kind !== '3D' &&
|
||||
fileKind.value !== '3D' &&
|
||||
(!isVideoPlaying.value || isCardOrOverlayHovered.value)
|
||||
)
|
||||
|
||||
const showDurationChips = computed(
|
||||
() =>
|
||||
!loading &&
|
||||
asset?.duration &&
|
||||
(asset?.user_metadata?.executionTimeInSeconds ||
|
||||
asset?.user_metadata?.duration) &&
|
||||
(!isVideoPlaying.value || isCardOrOverlayHovered.value)
|
||||
)
|
||||
|
||||
const showOutputCount = computed(
|
||||
() =>
|
||||
!loading &&
|
||||
context?.outputCount &&
|
||||
(!isVideoPlaying.value || isCardOrOverlayHovered.value)
|
||||
() => false // Remove output count for simplified version
|
||||
)
|
||||
|
||||
const handleCardClick = () => {
|
||||
if (asset) {
|
||||
actions.selectAsset(asset)
|
||||
if (adaptedAsset.value) {
|
||||
actions.selectAsset(adaptedAsset.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,7 +311,11 @@ const handleOverlayMouseLeave = () => {
|
||||
|
||||
const handleZoomClick = () => {
|
||||
if (asset) {
|
||||
galleryStore.openSingle(asset)
|
||||
emit('zoom', asset)
|
||||
}
|
||||
}
|
||||
|
||||
const handleImageLoaded = (dimensions: { width: number; height: number }) => {
|
||||
imageDimensions.value = dimensions
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
{{ fileName }}
|
||||
</h3>
|
||||
<div class="flex items-center text-xs text-zinc-400">
|
||||
<span>{{ asset.dimensions?.width }}x{{ asset.dimensions?.height }}</span>
|
||||
<span v-if="asset.dimensions"
|
||||
>{{ asset.dimensions?.width }}x{{ asset.dimensions?.height }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<div class="relative h-full w-full overflow-hidden rounded">
|
||||
<LazyImage
|
||||
v-if="asset.src"
|
||||
<div
|
||||
class="relative h-full w-full overflow-hidden rounded bg-zinc-200 dark-theme:bg-zinc-700/50"
|
||||
>
|
||||
<img
|
||||
v-if="shouldShowImage"
|
||||
:src="asset.src"
|
||||
:alt="asset.name"
|
||||
:container-class="'aspect-square'"
|
||||
:image-class="'w-full h-full object-cover'"
|
||||
class="h-full w-full object-contain"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
@@ -17,11 +18,35 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LazyImage from '@/components/common/LazyImage.vue'
|
||||
import { useImage } from '@vueuse/core'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
import type { AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
|
||||
const { asset } = defineProps<{
|
||||
asset: AssetMeta
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'image-loaded': [dimensions: { width: number; height: number }]
|
||||
}>()
|
||||
|
||||
// Use same image loading logic as AssetCard
|
||||
const { state, error, isReady } = useImage({
|
||||
src: asset.src ?? '',
|
||||
alt: asset.name
|
||||
})
|
||||
|
||||
const shouldShowImage = computed(() => asset.src && !error.value)
|
||||
|
||||
// Emit dimensions when image is loaded
|
||||
watch(isReady, (ready) => {
|
||||
if (ready && state.value) {
|
||||
const width = state.value.naturalWidth
|
||||
const height = state.value.naturalHeight
|
||||
if (width && height) {
|
||||
emit('image-loaded', { width, height })
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useAssetsSidebarTab } from '@/composables/sidebarTabs/useAssetsSidebarTab'
|
||||
import { useModelLibrarySidebarTab } from '@/composables/sidebarTabs/useModelLibrarySidebarTab'
|
||||
import { useNodeLibrarySidebarTab } from '@/composables/sidebarTabs/useNodeLibrarySidebarTab'
|
||||
import { useQueueSidebarTab } from '@/composables/sidebarTabs/useQueueSidebarTab'
|
||||
@@ -45,7 +46,8 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
|
||||
queue: 'menu.queue',
|
||||
'node-library': 'sideToolbar.nodeLibrary',
|
||||
'model-library': 'sideToolbar.modelLibrary',
|
||||
workflows: 'sideToolbar.workflows'
|
||||
workflows: 'sideToolbar.workflows',
|
||||
assets: 'sideToolbar.assets'
|
||||
}
|
||||
|
||||
const key = menubarLabelKeys[tab.id]
|
||||
@@ -102,6 +104,11 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
|
||||
* Register the core sidebar tabs.
|
||||
*/
|
||||
const registerCoreSidebarTabs = () => {
|
||||
// Only show AssetsSidebarTab in development mode
|
||||
if (import.meta.env.DEV) {
|
||||
registerSidebarTab(useAssetsSidebarTab())
|
||||
}
|
||||
|
||||
registerSidebarTab(useQueueSidebarTab())
|
||||
registerSidebarTab(useNodeLibrarySidebarTab())
|
||||
registerSidebarTab(useModelLibrarySidebarTab())
|
||||
|
||||
Reference in New Issue
Block a user