arcanus commited on
Commit
ba59f5e
·
verified ·
1 Parent(s): cfd31a2

Create timeline.js

Browse files
Files changed (1) hide show
  1. static/js/timeline.js +265 -0
static/js/timeline.js ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const TimelineManager = {
2
+ player: null,
3
+ zoomLevel: 1,
4
+
5
+ init: function(videoPlayer) {
6
+ this.player = videoPlayer;
7
+ this.bindEvents();
8
+ this.render();
9
+ },
10
+
11
+ bindEvents: function() {
12
+ document.getElementById('zoom-in').addEventListener('click', () => this.zoom(1.2));
13
+ document.getElementById('zoom-out').addEventListener('click', () => this.zoom(0.8));
14
+
15
+ document.getElementById('timeline').addEventListener('click', (e) => {
16
+ if (e.target.closest('.subtitle-marker')) return; // Don't handle clicks on subtitles
17
+
18
+ const timeline = e.currentTarget;
19
+ const timelineContent = document.getElementById('timeline-content');
20
+ const rect = timeline.getBoundingClientRect();
21
+ const scrollOffset = timeline.scrollLeft;
22
+ const clickX = e.clientX - rect.left;
23
+ const totalWidth = timelineContent.offsetWidth;
24
+ const scrollPercent = scrollOffset / (timelineContent.scrollWidth - timeline.offsetWidth);
25
+ const adjustedX = clickX + (scrollOffset * (totalWidth / timelineContent.scrollWidth));
26
+ const percentage = adjustedX / totalWidth;
27
+ const duration = this.player.duration() || 100;
28
+ const time = Math.max(0, Math.min(duration, duration * percentage));
29
+ this.player.currentTime(time);
30
+ });
31
+ },
32
+
33
+ zoom: function(factor) {
34
+ const timeline = document.getElementById('timeline');
35
+ const timelineContent = document.getElementById('timeline-content');
36
+ const currentScroll = timeline.scrollLeft;
37
+ const timelineWidth = timeline.offsetWidth;
38
+ const viewCenter = currentScroll + timelineWidth / 2;
39
+
40
+ this.zoomLevel *= (1/factor);
41
+ this.zoomLevel = Math.max(0.5, Math.min(this.zoomLevel, 4));
42
+
43
+ const duration = this.player.duration() || 100;
44
+ const subtitles = SubtitleManager.getSubtitles();
45
+ const maxSubtitleTime = subtitles.length > 0
46
+ ? Math.max(...subtitles.map(s => s.end))
47
+ : duration;
48
+ const contentDuration = Math.max(duration, maxSubtitleTime);
49
+ const totalWidth = Math.max(timelineWidth, timelineWidth * this.zoomLevel * (contentDuration / duration));
50
+ timelineContent.style.width = `${totalWidth}px`;
51
+
52
+ requestAnimationFrame(() => {
53
+ const zoomedCenter = (viewCenter / timelineWidth) * totalWidth;
54
+ timeline.scrollLeft = Math.max(0, Math.min(zoomedCenter - timelineWidth / 2, totalWidth - timelineWidth));
55
+ this.render();
56
+ });
57
+ },
58
+
59
+ render: function() {
60
+ const timelineContent = document.getElementById('timeline-content');
61
+ const timeline = document.getElementById('timeline');
62
+ timelineContent.innerHTML = '';
63
+
64
+ const duration = this.player ? (this.player.duration() || 100) : 100; // Use default duration if video not loaded
65
+ console.log("Timeline render - duration:", duration);
66
+ console.log("Timeline render - player:", this.player);
67
+ console.log("Subtitles:", SubtitleManager.getSubtitles());
68
+
69
+ const subtitles = SubtitleManager.getSubtitles();
70
+ const maxSubtitleTime = subtitles.length > 0
71
+ ? Math.max(...subtitles.map(s => s.end))
72
+ : duration;
73
+ const contentDuration = Math.max(duration, maxSubtitleTime);
74
+
75
+ // Ensure timeline-content is wide enough to contain all subtitles
76
+ const minWidth = Math.max(timeline.offsetWidth, timeline.offsetWidth * this.zoomLevel);
77
+ timelineContent.style.width = `${minWidth}px`;
78
+
79
+ // Render time markers
80
+ const baseInterval = 5; // základní interval v sekundách
81
+ const interval = Math.max(1, Math.floor(baseInterval / this.zoomLevel));
82
+
83
+ for (let time = 0; time <= contentDuration; time += interval) {
84
+ const marker = document.createElement('div');
85
+ marker.className = 'timeline-marker';
86
+ marker.style.left = `${(time / contentDuration) * 100}%`;
87
+ marker.style.width = '2px';
88
+
89
+ const label = document.createElement('div');
90
+ label.textContent = this.formatTime(time);
91
+ label.style.position = 'absolute';
92
+ label.style.top = '-20px';
93
+ label.style.transform = 'translateX(-50%)';
94
+
95
+ marker.appendChild(label);
96
+ timelineContent.appendChild(marker);
97
+ }
98
+
99
+ // Render subtitles
100
+ SubtitleManager.getSubtitles().forEach((subtitle, index) => {
101
+ const element = document.createElement('div');
102
+ element.className = 'subtitle-marker';
103
+ const leftPosition = (subtitle.start / contentDuration) * 100;
104
+ const width = ((subtitle.end - subtitle.start) / contentDuration) * 100;
105
+ element.style.left = `${leftPosition}%`;
106
+ element.style.width = `${width}%`;
107
+ element.textContent = subtitle.text;
108
+
109
+ // Přidání handlerů pro změnu velikosti
110
+ const leftHandle = document.createElement('div');
111
+ leftHandle.className = 'resize-handle left';
112
+ const rightHandle = document.createElement('div');
113
+ rightHandle.className = 'resize-handle right';
114
+ element.appendChild(leftHandle);
115
+ element.appendChild(rightHandle);
116
+
117
+ let isResizing = false;
118
+ let startX = 0;
119
+ let startLeft = 0;
120
+ let startWidth = 0;
121
+ let originalStart = 0;
122
+ let originalEnd = 0;
123
+
124
+ const onMouseMove = (e) => {
125
+ if (!isResizing) return;
126
+
127
+ const timeline = document.getElementById('timeline');
128
+ const timelineContent = document.getElementById('timeline-content');
129
+ const rect = timeline.getBoundingClientRect();
130
+ const scrollOffset = timeline.scrollLeft;
131
+ const duration = this.player.duration() || 100;
132
+ const subtitles = SubtitleManager.getSubtitles();
133
+ const maxSubtitleTime = subtitles.length > 0
134
+ ? Math.max(...subtitles.map(s => s.end))
135
+ : duration;
136
+ const contentDuration = Math.max(duration, maxSubtitleTime);
137
+
138
+ // Calculate mouse position relative to the scrolled content
139
+ const mouseX = e.clientX - rect.left + scrollOffset;
140
+ const totalWidth = timelineContent.scrollWidth; // Use scrollWidth instead of offsetWidth
141
+ const percentage = mouseX / totalWidth;
142
+ const newTime = Math.max(0, Math.min(contentDuration, contentDuration * percentage));
143
+
144
+ if (isResizing === 'left') {
145
+ if (newTime < subtitle.end && newTime >= 0) {
146
+ subtitle.start = newTime;
147
+ const startPercentage = (subtitle.start / duration) * 100;
148
+ const widthPercentage = ((subtitle.end - subtitle.start) / duration) * 100;
149
+ element.style.left = `${startPercentage}%`;
150
+ element.style.width = `${widthPercentage}%`;
151
+ }
152
+ } else if (newTime > subtitle.start && newTime <= contentDuration) {
153
+ subtitle.end = newTime;
154
+ const widthPercentage = ((subtitle.end - subtitle.start) / contentDuration) * 100;
155
+ element.style.width = `${widthPercentage}%`;
156
+ }
157
+
158
+ // Update both timeline and subtitle list
159
+ SubtitleManager.renderSubtitleList();
160
+ this.render();
161
+ };
162
+
163
+ const onMouseUp = () => {
164
+ if (isResizing) {
165
+ isResizing = false;
166
+ document.removeEventListener('mousemove', onMouseMove);
167
+ document.removeEventListener('mouseup', onMouseUp);
168
+ element.classList.remove('resizing');
169
+ SubtitleManager.renderSubtitleList();
170
+ }
171
+ };
172
+
173
+ element.addEventListener('mousedown', (e) => {
174
+ const handle = e.target.closest('.resize-handle');
175
+ if (handle) {
176
+ isResizing = handle.classList.contains('left') ? 'left' : 'right';
177
+ // Zabránit otevření modálního okna a propagaci události
178
+ e.stopPropagation();
179
+ e.preventDefault();
180
+
181
+ const rect = element.getBoundingClientRect();
182
+ const parentRect = element.parentElement.getBoundingClientRect();
183
+ startX = e.clientX - rect.left + (handle.classList.contains('left') ? 0 : rect.width);
184
+ startLeft = subtitle.start / (this.player.duration() || 100) * parentRect.width;
185
+ startWidth = rect.width;
186
+ originalStart = subtitle.start;
187
+ originalEnd = subtitle.end;
188
+
189
+ document.addEventListener('mousemove', onMouseMove);
190
+ document.addEventListener('mouseup', onMouseUp);
191
+
192
+ // Přidat třídu pro indikaci resize operace
193
+ element.classList.add('resizing');
194
+ }
195
+ });
196
+
197
+ element.addEventListener('click', (e) => {
198
+ const timeline = document.getElementById('timeline');
199
+
200
+ // Don't open modal if clicking resize handle or during resize
201
+ if (e.target.closest('.resize-handle') || element.classList.contains('resizing')) {
202
+ return;
203
+ }
204
+
205
+ if (e.ctrlKey || e.metaKey) {
206
+ // Ctrl+click to play from subtitle start
207
+ const timelineWidth = timeline.offsetWidth;
208
+ const duration = this.player.duration() || 100;
209
+ const position = (subtitle.start / duration) * timeline.scrollWidth;
210
+
211
+ // Center the subtitle in view
212
+ timeline.scrollLeft = Math.max(0, position - timelineWidth / 2);
213
+
214
+ // Set video time after ensuring subtitle is in view
215
+ requestAnimationFrame(() => {
216
+ this.player.currentTime(subtitle.start);
217
+ });
218
+ } else {
219
+ // Normal click for editing
220
+ const modal = new bootstrap.Modal(document.getElementById('editSubtitleModal'));
221
+ const textArea = document.getElementById('subtitleText');
222
+ textArea.value = subtitle.text;
223
+
224
+ const saveButton = document.getElementById('saveSubtitle');
225
+ const deleteButton = document.getElementById('deleteSubtitle');
226
+
227
+ const saveHandler = () => {
228
+ subtitle.text = textArea.value;
229
+ SubtitleManager.renderSubtitleList();
230
+ this.render();
231
+ modal.hide();
232
+ saveButton.removeEventListener('click', saveHandler);
233
+ deleteButton.removeEventListener('click', deleteHandler);
234
+ };
235
+
236
+ const deleteHandler = () => {
237
+ SubtitleManager.subtitles.splice(index, 1);
238
+ SubtitleManager.renderSubtitleList();
239
+ this.render();
240
+ modal.hide();
241
+ saveButton.removeEventListener('click', saveHandler);
242
+ deleteButton.removeEventListener('click', deleteHandler);
243
+ };
244
+
245
+ saveButton.addEventListener('click', saveHandler);
246
+ deleteButton.addEventListener('click', deleteHandler);
247
+ modal.show();
248
+ }
249
+ });
250
+
251
+ timelineContent.appendChild(element);
252
+ });
253
+ },
254
+
255
+ formatTime: function(seconds) {
256
+ const date = new Date(seconds * 1000);
257
+ const mm = date.getUTCMinutes();
258
+ const ss = date.getUTCSeconds();
259
+ return `${mm.toString().padStart(2, '0')}:${ss.toString().padStart(2, '0')}`;
260
+ },
261
+
262
+ updateTimeline: function() {
263
+ this.render();
264
+ }
265
+ };