mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
## Summary
Implements a comprehensive media asset card component system for the
Asset Manager sidebar, enabling display and interaction with various
media types (images, videos, audio, and 3D models).
## Changes
### New Components
- **MediaAssetCard**: Main card component for displaying media assets
- **Media type-specific components**: Specialized display logic for each
media type
- MediaImageTop/Bottom
- MediaVideoTop/Bottom
- MediaAudioTop/Bottom
- Media3DTop/Bottom
- **MediaAssetActions**: Top-left action buttons (delete, download, more
options)
- **MediaAssetMoreMenu**: Dropdown menu for additional actions
- **SquareChip**: Chip component for displaying duration and file format
with dark/light variants
- **MediaAssetButtonDivider**: Visual separator for button groups
### Features
- **Video playback**: Autoplay with native video controls
- Dynamic duration chip positioning based on control visibility
- Hides overlays when video is playing
- **Audio playback**: Audio icon with HTML5 audio element
- Duration chip with consistent positioning
- **3D model support**: Icon display for 3D assets
- **Selection state**: Proper hover and selected state handling with CSS
priority fixes
### Architecture Improvements
- **Domain-Driven Design structure**: Organized under
`src/platform/mediaAsset/` following DDD principles
- **Provide/Inject pattern**: Eliminates props drilling with
MediaAssetKey InjectionKey
- **Composable pattern**: `useMediaAssetActions` manages all action
handlers
- **Type safety**: Comprehensive TypeScript types for media assets and
actions
### UI/UX Enhancements
- **CardTop component**: Added custom class props for slot positioning
- **SquareChip component**: Backdrop blur effects with variant system
- **Lazy loading**: Image optimization with LazyImage component
- **Responsive states**: Loading, selected, and hover states
### Utilities
- **formatDuration**: Converts milliseconds to human-readable format
(45s, 1m 23s, 1h 2m)
## Testing
- Comprehensive Storybook stories for all media types
- Grid layout examples
- Loading and selected state demonstrations
## File Structure
```
src/platform/assets/
├── components/
│ ├── MediaAssetCard.vue
│ ├── MediaAssetCard.stories.ts
│ ├── MediaAssetActions.vue
│ ├── MediaAssetMoreMenu.vue
│ ├── MediaAssetButtonDivider.vue
│ ├── MediaImageTop.vue
│ ├── MediaImageBottom.vue
│ ├── MediaVideoTop.vue
│ ├── MediaVideoBottom.vue
│ ├── MediaAudioTop.vue
│ ├── MediaAudioBottom.vue
│ ├── Media3DTop.vue
│ └── Media3DBottom.vue
├── composables/
│ └── useMediaAssetActions.ts
└── schemas/
└── mediaAssetSchema.ts
```
## Screenshots
[media_asset_record.webm](https://github.com/user-attachments/assets/d13b5cc0-a262-4850-bb81-ca1daa0dd969)
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: github-actions <github-actions@github.com>
319 lines
7.4 KiB
TypeScript
319 lines
7.4 KiB
TypeScript
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
|
|
import type { AssetMeta } from '../schemas/mediaAssetSchema'
|
|
import MediaAssetCard from './MediaAssetCard.vue'
|
|
|
|
const meta: Meta<typeof MediaAssetCard> = {
|
|
title: 'AssetLibrary/MediaAssetCard',
|
|
component: MediaAssetCard,
|
|
argTypes: {
|
|
context: {
|
|
control: 'select',
|
|
options: ['input', 'output']
|
|
},
|
|
loading: {
|
|
control: 'boolean'
|
|
}
|
|
}
|
|
}
|
|
|
|
export default meta
|
|
type Story = StoryObj<typeof meta>
|
|
|
|
// Public sample media URLs
|
|
const SAMPLE_MEDIA = {
|
|
image1: 'https://i.imgur.com/OB0y6MR.jpg',
|
|
image2: 'https://i.imgur.com/CzXTtJV.jpg',
|
|
image3: 'https://farm9.staticflickr.com/8505/8441256181_4e98d8bff5_z_d.jpg',
|
|
video:
|
|
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
|
|
videoThumbnail:
|
|
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg',
|
|
audio: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'
|
|
}
|
|
|
|
const sampleAsset: AssetMeta = {
|
|
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: []
|
|
}
|
|
|
|
export const ImageAsset: Story = {
|
|
decorators: [
|
|
() => ({
|
|
template: '<div style="max-width: 280px;"><story /></div>'
|
|
})
|
|
],
|
|
args: {
|
|
context: { type: 'output', outputCount: 3 },
|
|
asset: sampleAsset,
|
|
loading: false
|
|
}
|
|
}
|
|
|
|
export const VideoAsset: Story = {
|
|
decorators: [
|
|
() => ({
|
|
template: '<div style="max-width: 280px;"><story /></div>'
|
|
})
|
|
],
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export const Model3DAsset: Story = {
|
|
decorators: [
|
|
() => ({
|
|
template: '<div style="max-width: 280px;"><story /></div>'
|
|
})
|
|
],
|
|
args: {
|
|
context: { type: 'input' },
|
|
asset: {
|
|
...sampleAsset,
|
|
id: 'asset-3',
|
|
name: 'Asset-3d-model.glb',
|
|
kind: '3D',
|
|
size: 7340032,
|
|
src: '',
|
|
dimensions: undefined,
|
|
duration: 18023
|
|
}
|
|
}
|
|
}
|
|
|
|
export const AudioAsset: Story = {
|
|
decorators: [
|
|
() => ({
|
|
template: '<div style="max-width: 280px;"><story /></div>'
|
|
})
|
|
],
|
|
args: {
|
|
context: { type: 'input' },
|
|
asset: {
|
|
...sampleAsset,
|
|
id: 'asset-3',
|
|
name: 'SoundHelix-Song.mp3',
|
|
kind: 'audio',
|
|
size: 5242880,
|
|
src: SAMPLE_MEDIA.audio,
|
|
dimensions: undefined,
|
|
duration: 23180
|
|
}
|
|
}
|
|
}
|
|
|
|
export const LoadingState: Story = {
|
|
decorators: [
|
|
() => ({
|
|
template: '<div style="max-width: 280px;"><story /></div>'
|
|
})
|
|
],
|
|
args: {
|
|
context: { type: 'input' },
|
|
asset: sampleAsset,
|
|
loading: true
|
|
}
|
|
}
|
|
|
|
export const LongFileName: Story = {
|
|
decorators: [
|
|
() => ({
|
|
template: '<div style="max-width: 280px;"><story /></div>'
|
|
})
|
|
],
|
|
args: {
|
|
context: { type: 'input' },
|
|
asset: {
|
|
...sampleAsset,
|
|
name: 'very-long-file-name-that-should-be-truncated-in-the-ui-to-prevent-overflow.png'
|
|
}
|
|
}
|
|
}
|
|
|
|
export const SelectedState: Story = {
|
|
decorators: [
|
|
() => ({
|
|
template: '<div style="max-width: 280px;"><story /></div>'
|
|
})
|
|
],
|
|
args: {
|
|
context: { type: 'output', outputCount: 2 },
|
|
asset: sampleAsset,
|
|
selected: true
|
|
}
|
|
}
|
|
|
|
export const WebMVideo: Story = {
|
|
decorators: [
|
|
() => ({
|
|
template: '<div style="max-width: 280px;"><story /></div>'
|
|
})
|
|
],
|
|
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: []
|
|
}
|
|
}
|
|
}
|
|
|
|
export const GifAnimation: Story = {
|
|
decorators: [
|
|
() => ({
|
|
template: '<div style="max-width: 280px;"><story /></div>'
|
|
})
|
|
],
|
|
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: []
|
|
}
|
|
}
|
|
}
|
|
|
|
export const GridLayout: Story = {
|
|
render: () => ({
|
|
components: { MediaAssetCard },
|
|
setup() {
|
|
const assets: AssetMeta[] = [
|
|
{
|
|
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: []
|
|
},
|
|
{
|
|
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: []
|
|
},
|
|
{
|
|
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: []
|
|
},
|
|
{
|
|
id: 'grid-4',
|
|
name: 'audio-file.mp3',
|
|
kind: 'audio',
|
|
size: 5242880,
|
|
duration: 180,
|
|
created_at: Date.now().toString(),
|
|
src: SAMPLE_MEDIA.audio,
|
|
tags: []
|
|
},
|
|
{
|
|
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: []
|
|
},
|
|
{
|
|
id: 'grid-6',
|
|
name: 'Asset-3d-model.glb',
|
|
kind: '3D',
|
|
size: 7340032,
|
|
src: '',
|
|
dimensions: undefined,
|
|
duration: 18023,
|
|
created_at: Date.now().toString(),
|
|
tags: []
|
|
},
|
|
{
|
|
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: []
|
|
}
|
|
]
|
|
return { assets }
|
|
},
|
|
template: `
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; padding: 16px;">
|
|
<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>
|
|
`
|
|
})
|
|
}
|