mxivideo/src/components/MediaLibrary.tsx

311 lines
10 KiB
TypeScript

import React, { useState, useRef } from 'react'
import { Upload, Video, Music, Image, File, Search, Filter, Grid, List } from 'lucide-react'
import { useMediaStore } from '../stores/useMediaStore'
import { useProjectStore } from '../stores/useProjectStore'
interface MediaItem {
id: string
name: string
type: 'video' | 'audio' | 'image'
path: string
size: number
duration?: number
thumbnail?: string
createdAt: string
}
interface MediaLibraryProps {
className?: string
}
const MediaLibrary: React.FC<MediaLibraryProps> = ({ className = '' }) => {
const fileInputRef = useRef<HTMLInputElement>(null)
const [searchTerm, setSearchTerm] = useState('')
const [filterType, setFilterType] = useState<'all' | 'video' | 'audio' | 'image'>('all')
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
const [dragOver, setDragOver] = useState(false)
const { setCurrentMedia } = useMediaStore()
const { currentProject, addVideoTrack, addAudioTrack } = useProjectStore()
// Mock media items - in real app this would come from the project
const [mediaItems, setMediaItems] = useState<MediaItem[]>([
{
id: '1',
name: 'sample_video.mp4',
type: 'video',
path: '/tmp/test_video.mp4',
size: 1024000,
duration: 30,
createdAt: '2025-07-10T09:00:00Z'
},
{
id: '2',
name: 'background_music.mp3',
type: 'audio',
path: '/tmp/audio.mp3',
size: 512000,
duration: 120,
createdAt: '2025-07-10T09:15:00Z'
}
])
// Filter media items
const filteredItems = mediaItems.filter(item => {
const matchesSearch = item.name.toLowerCase().includes(searchTerm.toLowerCase())
const matchesFilter = filterType === 'all' || item.type === filterType
return matchesSearch && matchesFilter
})
// Handle file upload
const handleFileUpload = (files: FileList | null) => {
if (!files) return
Array.from(files).forEach(file => {
const fileType = getFileType(file)
if (fileType) {
const newItem: MediaItem = {
id: crypto.randomUUID(),
name: file.name,
type: fileType,
path: URL.createObjectURL(file),
size: file.size,
createdAt: new Date().toISOString()
}
setMediaItems(prev => [...prev, newItem])
}
})
}
// Determine file type
const getFileType = (file: File): 'video' | 'audio' | 'image' | null => {
if (file.type.startsWith('video/')) return 'video'
if (file.type.startsWith('audio/')) return 'audio'
if (file.type.startsWith('image/')) return 'image'
return null
}
// Handle drag and drop
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
setDragOver(true)
}
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault()
setDragOver(false)
}
const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
setDragOver(false)
handleFileUpload(e.dataTransfer.files)
}
// Handle media item click
const handleMediaClick = (item: MediaItem) => {
setCurrentMedia(item.path)
}
// Handle adding to timeline
const handleAddToTimeline = (item: MediaItem) => {
if (!currentProject) return
if (item.type === 'video') {
addVideoTrack({
id: crypto.randomUUID(),
name: item.name,
file_path: item.path,
start_time: 0,
duration: item.duration || 0
})
} else if (item.type === 'audio') {
addAudioTrack({
id: crypto.randomUUID(),
name: item.name,
file_path: item.path,
start_time: 0,
duration: item.duration || 0,
volume: 1.0
})
}
}
// Format file size
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
// Format duration
const formatDuration = (seconds?: number): string => {
if (!seconds) return ''
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins}:${secs.toString().padStart(2, '0')}`
}
// Get icon for media type
const getMediaIcon = (type: string) => {
switch (type) {
case 'video': return <Video size={16} />
case 'audio': return <Music size={16} />
case 'image': return <Image size={16} />
default: return <File size={16} />
}
}
return (
<div className={`bg-white border-r border-secondary-200 flex flex-col ${className}`}>
{/* Header */}
<div className="p-4 border-b border-secondary-200">
<h2 className="text-lg font-semibold text-secondary-900 mb-4"></h2>
{/* Search and Filter */}
<div className="space-y-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-secondary-400" size={16} />
<input
type="text"
placeholder="搜索媒体文件..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="input pl-10"
/>
</div>
<div className="flex items-center justify-between">
<select
value={filterType}
onChange={(e) => setFilterType(e.target.value as any)}
className="input w-32"
>
<option value="all"></option>
<option value="video"></option>
<option value="audio"></option>
<option value="image"></option>
</select>
<div className="flex items-center space-x-1">
<button
onClick={() => setViewMode('grid')}
className={`p-2 rounded ${viewMode === 'grid' ? 'bg-primary-100 text-primary-600' : 'text-secondary-600 hover:bg-secondary-100'}`}
>
<Grid size={16} />
</button>
<button
onClick={() => setViewMode('list')}
className={`p-2 rounded ${viewMode === 'list' ? 'bg-primary-100 text-primary-600' : 'text-secondary-600 hover:bg-secondary-100'}`}
>
<List size={16} />
</button>
</div>
</div>
</div>
</div>
{/* Upload Area */}
<div
className={`m-4 border-2 border-dashed rounded-lg p-6 text-center transition-colors ${
dragOver
? 'border-primary-500 bg-primary-50'
: 'border-secondary-300 hover:border-secondary-400'
}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
>
<Upload className="mx-auto mb-2 text-secondary-400" size={24} />
<p className="text-sm text-secondary-600">
</p>
<p className="text-xs text-secondary-500 mt-1">
</p>
<input
ref={fileInputRef}
type="file"
multiple
accept="video/*,audio/*,image/*"
onChange={(e) => handleFileUpload(e.target.files)}
className="hidden"
/>
</div>
{/* Media Items */}
<div className="flex-1 overflow-y-auto p-4">
{filteredItems.length === 0 ? (
<div className="text-center text-secondary-500 py-8">
<File size={32} className="mx-auto mb-2" />
<p></p>
</div>
) : (
<div className={viewMode === 'grid' ? 'grid grid-cols-2 gap-3' : 'space-y-2'}>
{filteredItems.map(item => (
<div
key={item.id}
className={`border border-secondary-200 rounded-lg overflow-hidden hover:shadow-md transition-shadow cursor-pointer ${
viewMode === 'grid' ? 'aspect-square' : 'flex items-center p-3'
}`}
onClick={() => handleMediaClick(item)}
onDoubleClick={() => handleAddToTimeline(item)}
>
{viewMode === 'grid' ? (
<>
{/* Thumbnail */}
<div className="aspect-video bg-secondary-100 flex items-center justify-center">
{getMediaIcon(item.type)}
</div>
{/* Info */}
<div className="p-2">
<h3 className="text-sm font-medium text-secondary-900 truncate">
{item.name}
</h3>
<div className="flex items-center justify-between text-xs text-secondary-600 mt-1">
<span>{formatFileSize(item.size)}</span>
{item.duration && <span>{formatDuration(item.duration)}</span>}
</div>
</div>
</>
) : (
<>
{/* Icon */}
<div className="w-10 h-10 bg-secondary-100 rounded flex items-center justify-center mr-3">
{getMediaIcon(item.type)}
</div>
{/* Info */}
<div className="flex-1 min-w-0">
<h3 className="text-sm font-medium text-secondary-900 truncate">
{item.name}
</h3>
<div className="flex items-center space-x-2 text-xs text-secondary-600">
<span>{formatFileSize(item.size)}</span>
{item.duration && (
<>
<span></span>
<span>{formatDuration(item.duration)}</span>
</>
)}
</div>
</div>
</>
)}
</div>
))}
</div>
)}
</div>
</div>
)
}
export default MediaLibrary