./kaisetsu-app/src/components/Timeline.tsx
'use client';
import { useState } from 'react';
import TimelineBlock, { TimelineBlockData, VoiceTypeId } from './TimelineBlock';
import { List, FileText, Mic } from 'lucide-react';
interface TimelineProps {
blocks: TimelineBlockData[];
currentTime?: number;
tcOffsetSeconds?: number;
onBlockPlay?: (id: string, startTime: number) => void;
onBlockTextChange?: (id: string, text: string) => void;
onBlockVoiceTypeChange?: (id: string, voiceType: VoiceTypeId) => void;
onBlockSpeakerChange?: (id: string, speaker: string) => void;
onBlockDelete?: (id: string) => void;
onTimestampClick?: (time: number, blockType: 'dialogue' | 'narration' | 'silence') => void;
onAddNarration?: (afterTime: number) => void;
}
export default function Timeline({
blocks,
currentTime = 0,
tcOffsetSeconds,
onBlockPlay,
onBlockTextChange,
onBlockVoiceTypeChange,
onBlockSpeakerChange,
onBlockDelete,
onTimestampClick,
onAddNarration
}: TimelineProps) {
const [filter, setFilter] = useState<'all' | 'dialogue' | 'narration'>('all');
const filteredBlocks = blocks.filter((block) => {
if (filter === 'all') return true;
if (filter === 'dialogue') return block.type === 'dialogue' || block.type === 'silence';
if (filter === 'narration') return block.type === 'narration';
return true;
});
const getHighlightedBlock = () => {
return blocks.find(
(block) =>
block.startTime <= currentTime &&
(block.endTime === undefined || block.endTime >= currentTime)
);
};
const highlightedBlock = getHighlightedBlock();
const dialogueCount = blocks.filter((b) => b.type === 'dialogue').length;
const narrationCount = blocks.filter((b) => b.type === 'narration').length;
return (
<div className="bg-white rounded-xl border border-gray-200 flex flex-col h-full">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200">
<div className="flex items-center gap-2">
<List className="w-5 h-5 text-gray-600" />
<h2 className="font-semibold text-gray-900">タイムライン</h2>
<span className="text-xs bg-gray-100 text-gray-600 px-2 py-0.5 rounded-full">
{blocks.length} ブロック
</span>
</div>
{/* Filter tabs */}
<div className="flex items-center bg-gray-100 rounded-lg p-1">
<button
onClick={() => setFilter('all')}
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
filter === 'all'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
すべて
</button>
<button
onClick={() => setFilter('dialogue')}
className={`flex items-center gap-1 px-3 py-1 text-xs font-medium rounded-md transition-colors ${
filter === 'dialogue'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<FileText className="w-3 h-3" />
セリフ
<span className="bg-gray-200 text-gray-700 px-1.5 rounded">
{dialogueCount}
</span>
</button>
<button
onClick={() => setFilter('narration')}
className={`flex items-center gap-1 px-3 py-1 text-xs font-medium rounded-md transition-colors ${
filter === 'narration'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<Mic className="w-3 h-3" />
解説音声
<span className="bg-gray-200 text-gray-700 px-1.5 rounded">
{narrationCount}
</span>
</button>
</div>
</div>
{/* Timeline content */}
<div className="flex-1 overflow-y-auto p-4">
{filteredBlocks.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64 text-gray-400">
<List className="w-12 h-12 mb-3 opacity-50" />
<p className="text-sm">タイムラインにブロックがありません</p>
<p className="text-xs mt-1">動画を解析するか、解説音声を追加してください</p>
</div>
) : (
<div className="space-y-0">
{filteredBlocks.map((block) => (
<TimelineBlock
key={block.id}
block={block}
isHighlighted={highlightedBlock?.id === block.id}
tcOffsetSeconds={tcOffsetSeconds}
onPlay={onBlockPlay}
onTextChange={onBlockTextChange}
onVoiceTypeChange={onBlockVoiceTypeChange}
onSpeakerChange={onBlockSpeakerChange}
onDelete={onBlockDelete}
onTimestampClick={onTimestampClick}
onAddNarration={onAddNarration}
/>
))}
</div>
)}
</div>
</div>
);
}