193 lines
7.2 KiB
TypeScript
193 lines
7.2 KiB
TypeScript
import React from 'react'
|
|
import { Plus, Gauge, Download, Maximize2, Sparkles, FileUp, FileDown, ZoomOut, ZoomIn } from 'lucide-react'
|
|
import { Button } from '../../components/ui/button'
|
|
import { Tooltip } from '../../components/ui/tooltip'
|
|
import type { TimelineClip, Track, SubtitleClip } from '../../types/project-model'
|
|
|
|
interface TimelineToolbarProps {
|
|
selectedClip: TimelineClip | null
|
|
updateClip: (id: string, updates: Partial<TimelineClip>) => void
|
|
getMaxClipDuration: (clip: TimelineClip) => number
|
|
setShowExportModal: (v: boolean) => void
|
|
handleResetLayout: () => void
|
|
canUseIcLora: boolean
|
|
onICLoraClip: (clip: TimelineClip) => void
|
|
tracks: Track[]
|
|
subtitleFileInputRef: React.RefObject<HTMLInputElement>
|
|
handleImportSrt: (e: React.ChangeEvent<HTMLInputElement>) => void
|
|
handleExportSrt: () => void
|
|
subtitles: SubtitleClip[]
|
|
zoom: number
|
|
setZoom: (z: number) => void
|
|
getMinZoom: () => number
|
|
centerOnPlayheadRef: React.MutableRefObject<boolean>
|
|
handleFitToView: () => void
|
|
}
|
|
|
|
export function TimelineToolbar({
|
|
selectedClip, updateClip, getMaxClipDuration,
|
|
setShowExportModal, handleResetLayout,
|
|
canUseIcLora, onICLoraClip,
|
|
tracks, subtitleFileInputRef, handleImportSrt, handleExportSrt, subtitles,
|
|
zoom, setZoom, getMinZoom, centerOnPlayheadRef, handleFitToView,
|
|
}: TimelineToolbarProps) {
|
|
return (
|
|
<div className="h-9 bg-zinc-900 border-t border-zinc-800 flex items-center px-3 gap-2 flex-shrink-0">
|
|
<Button variant="outline" size="sm" className="h-6 border-zinc-700 text-zinc-400 text-[10px] px-2">
|
|
<Plus className="h-3 w-3 mr-1" />
|
|
Add Clip
|
|
</Button>
|
|
|
|
{selectedClip && (
|
|
<>
|
|
<div className="w-px h-4 bg-zinc-700" />
|
|
<div className="flex items-center gap-1.5 text-[10px] text-zinc-400">
|
|
<Gauge className="h-3 w-3" />
|
|
<select
|
|
value={selectedClip.speed}
|
|
onChange={(e) => {
|
|
const newSpeed = parseFloat(e.target.value)
|
|
const oldSpeed = selectedClip.speed
|
|
let newDuration = selectedClip.duration * (oldSpeed / newSpeed)
|
|
const maxDur = getMaxClipDuration({ ...selectedClip, speed: newSpeed })
|
|
newDuration = Math.min(newDuration, maxDur)
|
|
newDuration = Math.max(0.5, newDuration)
|
|
updateClip(selectedClip.id, { speed: newSpeed, duration: newDuration })
|
|
}}
|
|
className="bg-zinc-800 border border-zinc-700 rounded px-1.5 py-0.5 text-[10px] text-white"
|
|
>
|
|
<option value={0.25}>0.25x</option>
|
|
<option value={0.5}>0.5x</option>
|
|
<option value={0.75}>0.75x</option>
|
|
<option value={1}>1x</option>
|
|
<option value={1.25}>1.25x</option>
|
|
<option value={1.5}>1.5x</option>
|
|
<option value={2}>2x</option>
|
|
<option value={4}>4x</option>
|
|
</select>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<div className="w-px h-4 bg-zinc-700" />
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-6 border-zinc-700 text-zinc-400 text-[10px] px-2"
|
|
onClick={() => setShowExportModal(true)}
|
|
>
|
|
<Download className="h-3 w-3 mr-1" />
|
|
Export
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-6 border-zinc-700 text-zinc-400 text-[10px] px-2"
|
|
onClick={handleResetLayout}
|
|
title="Reset panel sizes to default"
|
|
>
|
|
<Maximize2 className="h-3 w-3 mr-1" />
|
|
Layout
|
|
</Button>
|
|
|
|
<div className="w-px h-4 bg-zinc-700" />
|
|
|
|
{canUseIcLora && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-6 border-amber-700/50 text-amber-400 text-[10px] px-2 hover:bg-amber-900/30"
|
|
onClick={() => {
|
|
if (selectedClip?.type === 'video') {
|
|
onICLoraClip(selectedClip)
|
|
}
|
|
}}
|
|
disabled={selectedClip?.type !== 'video'}
|
|
title="Open IC-LoRA style transfer panel"
|
|
>
|
|
<Sparkles className="h-3 w-3 mr-1" />
|
|
IC-LoRA
|
|
</Button>
|
|
)}
|
|
|
|
|
|
{/* Subtitle import/export */}
|
|
{tracks.some(t => t.type === 'subtitle') && (
|
|
<>
|
|
<div className="w-px h-4 bg-zinc-700" />
|
|
<div className="flex items-center gap-1">
|
|
<button
|
|
onClick={() => subtitleFileInputRef.current?.click()}
|
|
className="h-6 px-2 rounded bg-amber-900/30 border border-amber-700/30 text-amber-400 hover:bg-amber-900/50 text-[10px] flex items-center gap-1 transition-colors"
|
|
title="Import SRT subtitles"
|
|
>
|
|
<FileUp className="h-3 w-3" />
|
|
Import SRT
|
|
</button>
|
|
<button
|
|
onClick={handleExportSrt}
|
|
disabled={subtitles.length === 0}
|
|
className="h-6 px-2 rounded bg-amber-900/30 border border-amber-700/30 text-amber-400 hover:bg-amber-900/50 text-[10px] flex items-center gap-1 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
|
title="Export SRT subtitles"
|
|
>
|
|
<FileDown className="h-3 w-3" />
|
|
Export SRT
|
|
</button>
|
|
</div>
|
|
<input
|
|
ref={subtitleFileInputRef}
|
|
type="file"
|
|
accept=".srt"
|
|
onChange={handleImportSrt}
|
|
className="hidden"
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{/* Spacer */}
|
|
<div className="flex-1" />
|
|
|
|
{/* Zoom slider bar */}
|
|
<div className="flex items-center gap-2">
|
|
<Tooltip content="Zoom out (-)">
|
|
<button
|
|
onClick={() => { centerOnPlayheadRef.current = true; setZoom(Math.max(getMinZoom(), +(zoom - 0.25).toFixed(2))) }}
|
|
className="p-0.5 rounded hover:bg-zinc-800 text-zinc-500 hover:text-zinc-300 transition-colors"
|
|
>
|
|
<ZoomOut className="h-3.5 w-3.5" />
|
|
</button>
|
|
</Tooltip>
|
|
<input
|
|
type="range"
|
|
min={Math.max(1, Math.round(getMinZoom() * 100))}
|
|
max={400}
|
|
step={5}
|
|
value={Math.round(zoom * 100)}
|
|
onChange={(e) => { centerOnPlayheadRef.current = true; setZoom(Math.max(getMinZoom(), +(parseInt(e.target.value) / 100).toFixed(2))) }}
|
|
className="w-28 h-1 accent-blue-500 cursor-pointer"
|
|
title={`Zoom: ${Math.round(zoom * 100)}%`}
|
|
/>
|
|
<Tooltip content="Zoom in (+)">
|
|
<button
|
|
onClick={() => { centerOnPlayheadRef.current = true; setZoom(Math.min(4, +(zoom + 0.25).toFixed(2))) }}
|
|
className="p-0.5 rounded hover:bg-zinc-800 text-zinc-500 hover:text-zinc-300 transition-colors"
|
|
>
|
|
<ZoomIn className="h-3.5 w-3.5" />
|
|
</button>
|
|
</Tooltip>
|
|
<span className="text-[10px] text-zinc-500 tabular-nums w-8 text-right">{Math.round(zoom * 100)}%</span>
|
|
<Tooltip content="Fit to view (Ctrl+0)">
|
|
<button
|
|
onClick={handleFitToView}
|
|
className="p-0.5 rounded hover:bg-zinc-800 text-zinc-500 hover:text-zinc-300 transition-colors ml-0.5"
|
|
>
|
|
<Maximize2 className="h-3.5 w-3.5" />
|
|
</button>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|