File size: 8,689 Bytes
ae238b3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
import { useCallback, useEffect } from 'react';
import { AlignedSegment } from '../services/transcriptionApi';
import { SegmentWithTrack } from '../utils/trackUtils';
import { formatTime } from '../utils/subtitleUtils';

interface UseTimelineRendererOptions {
  canvasRef: React.RefObject<HTMLCanvasElement>;
  canvasSize: { width: number; height: number };
  segmentsWithTracks: SegmentWithTrack[];
  displaySegments: AlignedSegment[];
  currentTime: number;
  activeSegmentIndex: number | null;
  selectedSegmentIndex: number | null;
  hoveredSegment: number | null;
  isDragging: boolean;
  dragSegmentIndex: number | null;
  mediaDuration: number;
  geometryUtils: {
    timeToX: (time: number) => number;
    trackToY: (track: number) => number;
    timelineWidth: number;
  };
  constants: {
    TRACK_HEIGHT: number;
    TIMELINE_PADDING: number;
    PIXELS_PER_SECOND: number;
  };
}

export const useTimelineRenderer = ({
  canvasRef,
  canvasSize,
  segmentsWithTracks,
  displaySegments,
  currentTime,
  activeSegmentIndex,
  selectedSegmentIndex,
  hoveredSegment,
  isDragging,
  dragSegmentIndex,
  mediaDuration,
  geometryUtils,
  constants,
}: UseTimelineRendererOptions) => {
  const { timeToX, trackToY, timelineWidth } = geometryUtils;
  const { TRACK_HEIGHT, TIMELINE_PADDING, PIXELS_PER_SECOND } = constants;

  // Draw the timeline
  const draw = useCallback(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    // Set canvas size
    canvas.width = canvasSize.width;
    canvas.height = canvasSize.height;

    // Clear canvas
    ctx.fillStyle = '#0f172a'; // bg-slate-900
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    // Draw timeline base line
    ctx.strokeStyle = '#4B5563'; // gray-600
    ctx.lineWidth = 1;
    ctx.beginPath();
    const baseY = canvas.height / 2;
    ctx.moveTo(TIMELINE_PADDING, baseY);
    ctx.lineTo(timelineWidth - TIMELINE_PADDING, baseY);
    ctx.stroke();

    // Draw time markers with dynamic intervals
    const getOptimalTimeInterval = () => {
      const pixelsPerSecond = PIXELS_PER_SECOND;
      const minSpacing = 120; // Increased from 80 to give more space between markers

      // Calculate what time interval gives us reasonable spacing
      const minTimeInterval = minSpacing / pixelsPerSecond;

      // Choose appropriate intervals based on duration and zoom
      if (minTimeInterval <= 1) return { major: 5, minor: 1 };
      if (minTimeInterval <= 5) return { major: 10, minor: 2 };
      if (minTimeInterval <= 10) return { major: 30, minor: 5 };
      if (minTimeInterval <= 30) return { major: 60, minor: 10 };
      if (minTimeInterval <= 60) return { major: 300, minor: 60 }; // 5min major, 1min minor
      if (minTimeInterval <= 300) return { major: 600, minor: 120 }; // 10min major, 2min minor
      return { major: 1800, minor: 300 }; // 30min major, 5min minor
    };

    const { major: majorInterval, minor: minorInterval } = getOptimalTimeInterval();

    // Draw background grid lines for better visual organization
    ctx.strokeStyle = '#1E293B'; // slate-800 (very subtle)
    ctx.lineWidth = 1;
    for (let time = 0; time <= mediaDuration; time += minorInterval) {
      const x = timeToX(time);
      ctx.beginPath();
      ctx.moveTo(x, 0);
      ctx.lineTo(x, canvas.height - 40); // Don't overlap with time labels
      ctx.stroke();
    }

    // Draw minor markers (shorter, more visible than before)
    ctx.strokeStyle = '#64748B'; // slate-500 (more visible than gray-700)
    ctx.lineWidth = 1;
    for (let time = 0; time <= mediaDuration; time += minorInterval) {
        const x = timeToX(time);
        ctx.beginPath();
        ctx.moveTo(x, canvas.height - 15);
        ctx.lineTo(x, canvas.height - 5);
        ctx.stroke();
    }

    // Draw major markers (taller, much more prominent)
    ctx.strokeStyle = '#94A3B8'; // slate-400 (much more visible)
    ctx.fillStyle = '#F1F5F9'; // slate-100 (bright white-ish for text)
    ctx.font = 'bold 13px system-ui'; // Slightly larger and bold
    ctx.lineWidth = 2; // Thicker lines for major markers

    for (let time = 0; time <= mediaDuration; time += majorInterval) {
      const x = timeToX(time);

      // Draw time label with special handling for 0:00 to avoid clipping
      const timeText = formatTime(time);
      ctx.fillStyle = '#F1F5F9'; // slate-100 (bright white-ish)

      if (time === 0) {
        // For 0:00, align left and shift right to avoid clipping
        ctx.textAlign = 'left';
        ctx.fillText(timeText, x + 4, canvas.height - 20);
      } else {
        // For all other times, center align as normal
        ctx.textAlign = 'center';
        ctx.fillText(timeText, x, canvas.height - 20);
      }
     }

    // Draw segments
    segmentsWithTracks.forEach((segment) => {
      // Find the original segment index in displaySegments
      const originalIndex = displaySegments.findIndex(s =>
        s.start === segment.start && s.end === segment.end && s.text === segment.text
      );

      const x = timeToX(segment.start);
      // Calculate actual width based on time duration, don't enforce minimum width for rendering
      const actualWidth = timeToX(segment.end) - timeToX(segment.start);
      const width = Math.max(actualWidth, 2); // Minimum 2px so segments are always visible
      const y = trackToY(segment.track);
      const height = TRACK_HEIGHT;

      // Draw all segments (scrolling is handled by container)
      {
        // Determine segment color based on original segment index, not track index
        let fillColor = '#374151'; // gray-700 (default)
        let strokeColor = '#4B5563'; // gray-600
        let textColor = '#D1D5DB'; // gray-300

        // Priority order: dragging > selected > active > hovered
        // Use originalIndex for all comparisons to maintain consistency during drag operations
        if (isDragging && dragSegmentIndex === originalIndex) {
          // Special styling for segment being dragged
          fillColor = '#DC2626'; // red-600 (dragging indicator)
          strokeColor = '#EF4444'; // red-500
          textColor = '#FFFFFF';
        } else if (selectedSegmentIndex === originalIndex) {
          fillColor = '#D97706'; // yellow-600
          strokeColor = '#FBBF24'; // yellow-400
          textColor = '#FFFFFF';
        } else if (activeSegmentIndex === originalIndex && selectedSegmentIndex === null) {
          // Don't highlight active segment in blue when there's a selected segment
          fillColor = '#2563EB'; // blue-600
          strokeColor = '#3B82F6'; // blue-500
          textColor = '#FFFFFF';
        } else if (hoveredSegment === originalIndex && !isDragging) {
          // Only show hover state when not dragging
          fillColor = '#4B5563'; // gray-600
          strokeColor = '#6B7280'; // gray-500
        }

        // Draw segment rectangle
        ctx.fillStyle = fillColor;
        ctx.fillRect(x, y, width, height);

        // Draw segment border
        ctx.strokeStyle = strokeColor;
        ctx.lineWidth = 1;
        ctx.strokeRect(x, y, width, height);

        // Draw segment text
        ctx.fillStyle = textColor;
        ctx.font = '12px system-ui';
        ctx.textAlign = 'left';

        // Clip text to segment width
        ctx.save();
        ctx.beginPath();
        ctx.rect(x + 4, y, width - 8, height);
        ctx.clip();

        const textY = y + height / 2 + 4; // Center vertically
        ctx.fillText(segment.text, x + 4, textY);
        ctx.restore();

        // Draw resize handles for selected segment
        if (selectedSegmentIndex === originalIndex) {
          const handleWidth = 8;
          ctx.fillStyle = '#3B82F6'; // blue-500

          // Left handle
          ctx.fillRect(x, y, handleWidth, height);

          // Right handle
          ctx.fillRect(x + width - handleWidth, y, handleWidth, height);
        }
      }
    });

    // Draw progress indicator
    const progressX = timeToX(currentTime);
    ctx.strokeStyle = '#EF4444'; // red-500
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(progressX, 0);
    ctx.lineTo(progressX, canvas.height);
    ctx.stroke();
  }, [
    canvasRef,
    canvasSize,
    segmentsWithTracks,
    displaySegments,
    currentTime,
    activeSegmentIndex,
    selectedSegmentIndex,
    hoveredSegment,
    isDragging,
    dragSegmentIndex,
    mediaDuration,
    timeToX,
    trackToY,
    timelineWidth,
    TRACK_HEIGHT,
    TIMELINE_PADDING,
    PIXELS_PER_SECOND,
  ]);

  // Redraw when dependencies change
  useEffect(() => {
    draw();
  }, [draw]);

  return { draw };
};