311 lines
10 KiB
TypeScript
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
|