fix
This commit is contained in:
parent
b64c0e7452
commit
b7887caf9d
|
|
@ -1,228 +0,0 @@
|
||||||
import React, { useRef, useEffect, useState } from 'react'
|
|
||||||
import { Play, Pause, SkipBack, SkipForward, Volume2, Scissors } from 'lucide-react'
|
|
||||||
import { useProjectStore } from '../stores/useProjectStore'
|
|
||||||
import { useMediaStore } from '../stores/useMediaStore'
|
|
||||||
|
|
||||||
interface TimelineProps {
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Timeline: React.FC<TimelineProps> = ({ className = '' }) => {
|
|
||||||
const timelineRef = useRef<HTMLDivElement>(null)
|
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
|
||||||
const [dragStartX, setDragStartX] = useState(0)
|
|
||||||
|
|
||||||
const { currentProject, setTimelinePosition, setTimelineZoom } = useProjectStore()
|
|
||||||
const {
|
|
||||||
isPlaying,
|
|
||||||
currentTime,
|
|
||||||
duration,
|
|
||||||
volume,
|
|
||||||
setPlaying,
|
|
||||||
setCurrentTime,
|
|
||||||
setVolume
|
|
||||||
} = useMediaStore()
|
|
||||||
|
|
||||||
const timeline = currentProject?.timeline || { duration: 0, zoom: 1, position: 0 }
|
|
||||||
const videoTracks = currentProject?.video_tracks || []
|
|
||||||
const audioTracks = currentProject?.audio_tracks || []
|
|
||||||
|
|
||||||
// Handle timeline scrubbing
|
|
||||||
const handleTimelineClick = (e: React.MouseEvent) => {
|
|
||||||
if (!timelineRef.current) return
|
|
||||||
|
|
||||||
const rect = timelineRef.current.getBoundingClientRect()
|
|
||||||
const x = e.clientX - rect.left
|
|
||||||
const percentage = x / rect.width
|
|
||||||
const newTime = percentage * timeline.duration
|
|
||||||
|
|
||||||
setCurrentTime(newTime)
|
|
||||||
setTimelinePosition(newTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle track dragging
|
|
||||||
const handleTrackMouseDown = (e: React.MouseEvent, trackId: string, trackType: 'video' | 'audio') => {
|
|
||||||
e.preventDefault()
|
|
||||||
setIsDragging(true)
|
|
||||||
setDragStartX(e.clientX)
|
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
|
||||||
if (!isDragging) return
|
|
||||||
|
|
||||||
const deltaX = e.clientX - dragStartX
|
|
||||||
const timeDelta = (deltaX / (timelineRef.current?.clientWidth || 1)) * timeline.duration
|
|
||||||
|
|
||||||
// Update track position
|
|
||||||
// This would update the track's start_time
|
|
||||||
console.log(`Moving ${trackType} track ${trackId} by ${timeDelta}s`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
|
||||||
setIsDragging(false)
|
|
||||||
document.removeEventListener('mousemove', handleMouseMove)
|
|
||||||
document.removeEventListener('mouseup', handleMouseUp)
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', handleMouseMove)
|
|
||||||
document.addEventListener('mouseup', handleMouseUp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format time for display
|
|
||||||
const formatTime = (seconds: number): string => {
|
|
||||||
const mins = Math.floor(seconds / 60)
|
|
||||||
const secs = Math.floor(seconds % 60)
|
|
||||||
return `${mins}:${secs.toString().padStart(2, '0')}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate track position and width
|
|
||||||
const getTrackStyle = (startTime: number, duration: number) => {
|
|
||||||
const left = (startTime / timeline.duration) * 100
|
|
||||||
const width = (duration / timeline.duration) * 100
|
|
||||||
return {
|
|
||||||
left: `${left}%`,
|
|
||||||
width: `${width}%`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`bg-secondary-800 border-t border-secondary-700 ${className}`}>
|
|
||||||
{/* Timeline Controls */}
|
|
||||||
<div className="flex items-center justify-between p-4 border-b border-secondary-700">
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<button
|
|
||||||
onClick={() => setCurrentTime(Math.max(0, currentTime - 10))}
|
|
||||||
className="text-white hover:text-primary-400 transition-colors"
|
|
||||||
>
|
|
||||||
<SkipBack size={20} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => setPlaying(!isPlaying)}
|
|
||||||
className="w-12 h-12 bg-primary-600 hover:bg-primary-700 rounded-full flex items-center justify-center text-white transition-colors"
|
|
||||||
>
|
|
||||||
{isPlaying ? <Pause size={20} /> : <Play size={20} />}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => setCurrentTime(Math.min(duration, currentTime + 10))}
|
|
||||||
className="text-white hover:text-primary-400 transition-colors"
|
|
||||||
>
|
|
||||||
<SkipForward size={20} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2 ml-8">
|
|
||||||
<Volume2 className="text-white" size={16} />
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="1"
|
|
||||||
step="0.01"
|
|
||||||
value={volume}
|
|
||||||
onChange={(e) => setVolume(parseFloat(e.target.value))}
|
|
||||||
className="w-20"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<span className="text-white text-sm">
|
|
||||||
{formatTime(currentTime)} / {formatTime(timeline.duration)}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<span className="text-white text-sm">缩放:</span>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0.1"
|
|
||||||
max="5"
|
|
||||||
step="0.1"
|
|
||||||
value={timeline.zoom}
|
|
||||||
onChange={(e) => setTimelineZoom(parseFloat(e.target.value))}
|
|
||||||
className="w-20"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Timeline Ruler */}
|
|
||||||
<div className="relative h-8 bg-secondary-900 border-b border-secondary-700">
|
|
||||||
<div className="absolute inset-0 flex items-center">
|
|
||||||
{Array.from({ length: Math.ceil(timeline.duration / 10) + 1 }, (_, i) => {
|
|
||||||
const time = i * 10
|
|
||||||
const left = (time / timeline.duration) * 100
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="absolute text-xs text-secondary-400"
|
|
||||||
style={{ left: `${left}%` }}
|
|
||||||
>
|
|
||||||
{formatTime(time)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Playhead */}
|
|
||||||
<div
|
|
||||||
className="absolute top-0 bottom-0 w-0.5 bg-red-500 z-10"
|
|
||||||
style={{ left: `${(currentTime / timeline.duration) * 100}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Timeline Tracks */}
|
|
||||||
<div
|
|
||||||
ref={timelineRef}
|
|
||||||
className="relative min-h-32 bg-secondary-100 overflow-y-auto"
|
|
||||||
onClick={handleTimelineClick}
|
|
||||||
>
|
|
||||||
{/* Video Tracks */}
|
|
||||||
{videoTracks.map((track, index) => (
|
|
||||||
<div key={track.id} className="timeline-track">
|
|
||||||
<div className="absolute left-0 top-0 bottom-0 w-24 bg-secondary-800 border-r border-secondary-700 flex items-center px-2">
|
|
||||||
<span className="text-white text-sm truncate">视频 {index + 1}</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="timeline-clip bg-primary-500 hover:bg-primary-600 cursor-pointer"
|
|
||||||
style={getTrackStyle(track.start_time, track.duration)}
|
|
||||||
onMouseDown={(e) => handleTrackMouseDown(e, track.id, 'video')}
|
|
||||||
>
|
|
||||||
<div className="p-2 text-white text-xs truncate">
|
|
||||||
{track.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Audio Tracks */}
|
|
||||||
{audioTracks.map((track, index) => (
|
|
||||||
<div key={track.id} className="timeline-track">
|
|
||||||
<div className="absolute left-0 top-0 bottom-0 w-24 bg-secondary-800 border-r border-secondary-700 flex items-center px-2">
|
|
||||||
<span className="text-white text-sm truncate">音频 {index + 1}</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="timeline-clip bg-green-500 hover:bg-green-600 cursor-pointer"
|
|
||||||
style={getTrackStyle(track.start_time, track.duration)}
|
|
||||||
onMouseDown={(e) => handleTrackMouseDown(e, track.id, 'audio')}
|
|
||||||
>
|
|
||||||
<div className="p-2 text-white text-xs truncate">
|
|
||||||
{track.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Empty state */}
|
|
||||||
{videoTracks.length === 0 && audioTracks.length === 0 && (
|
|
||||||
<div className="flex items-center justify-center h-32 text-secondary-500">
|
|
||||||
<div className="text-center">
|
|
||||||
<Scissors size={32} className="mx-auto mb-2" />
|
|
||||||
<p>拖拽媒体文件到这里开始编辑</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Timeline
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Download, Save, Scissors } from 'lucide-react'
|
import { Download, Save, Scissors } from 'lucide-react'
|
||||||
import VideoPreview from '../components/VideoPreview'
|
import VideoPreview from '../components/VideoPreview'
|
||||||
import Timeline from '../components/Timeline'
|
|
||||||
import MediaLibrary from '../components/MediaLibrary'
|
import MediaLibrary from '../components/MediaLibrary'
|
||||||
import { useProjectStore } from '../stores/useProjectStore'
|
import { useProjectStore } from '../stores/useProjectStore'
|
||||||
|
|
||||||
|
|
@ -71,9 +70,6 @@ const EditorPage: React.FC = () => {
|
||||||
<div className="flex-1 bg-black p-4">
|
<div className="flex-1 bg-black p-4">
|
||||||
<VideoPreview className="w-full h-full" />
|
<VideoPreview className="w-full h-full" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Timeline */}
|
|
||||||
<Timeline className="h-48" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Properties Panel */}
|
{/* Properties Panel */}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue