Spaces:
Paused
Paused
| const TimelineManager = { | |
| player: null, | |
| zoomLevel: 1, | |
| init: function(videoPlayer) { | |
| this.player = videoPlayer; | |
| this.bindEvents(); | |
| this.render(); | |
| }, | |
| bindEvents: function() { | |
| document.getElementById('zoom-in').addEventListener('click', () => this.zoom(1.2)); | |
| document.getElementById('zoom-out').addEventListener('click', () => this.zoom(0.8)); | |
| document.getElementById('timeline').addEventListener('click', (e) => { | |
| if (e.target.closest('.subtitle-marker')) return; // Don't handle clicks on subtitles | |
| const timeline = e.currentTarget; | |
| const timelineContent = document.getElementById('timeline-content'); | |
| const rect = timeline.getBoundingClientRect(); | |
| const scrollOffset = timeline.scrollLeft; | |
| const clickX = e.clientX - rect.left; | |
| const totalWidth = timelineContent.offsetWidth; | |
| const scrollPercent = scrollOffset / (timelineContent.scrollWidth - timeline.offsetWidth); | |
| const adjustedX = clickX + (scrollOffset * (totalWidth / timelineContent.scrollWidth)); | |
| const percentage = adjustedX / totalWidth; | |
| const duration = this.player.duration() || 100; | |
| const time = Math.max(0, Math.min(duration, duration * percentage)); | |
| this.player.currentTime(time); | |
| }); | |
| }, | |
| zoom: function(factor) { | |
| const timeline = document.getElementById('timeline'); | |
| const timelineContent = document.getElementById('timeline-content'); | |
| const currentScroll = timeline.scrollLeft; | |
| const timelineWidth = timeline.offsetWidth; | |
| const viewCenter = currentScroll + timelineWidth / 2; | |
| this.zoomLevel *= (1/factor); | |
| this.zoomLevel = Math.max(0.5, Math.min(this.zoomLevel, 4)); | |
| const duration = this.player.duration() || 100; | |
| const subtitles = SubtitleManager.getSubtitles(); | |
| const maxSubtitleTime = subtitles.length > 0 | |
| ? Math.max(...subtitles.map(s => s.end)) | |
| : duration; | |
| const contentDuration = Math.max(duration, maxSubtitleTime); | |
| const totalWidth = Math.max(timelineWidth, timelineWidth * this.zoomLevel * (contentDuration / duration)); | |
| timelineContent.style.width = `${totalWidth}px`; | |
| requestAnimationFrame(() => { | |
| const zoomedCenter = (viewCenter / timelineWidth) * totalWidth; | |
| timeline.scrollLeft = Math.max(0, Math.min(zoomedCenter - timelineWidth / 2, totalWidth - timelineWidth)); | |
| this.render(); | |
| }); | |
| }, | |
| render: function() { | |
| const timelineContent = document.getElementById('timeline-content'); | |
| const timeline = document.getElementById('timeline'); | |
| timelineContent.innerHTML = ''; | |
| const duration = this.player ? (this.player.duration() || 100) : 100; // Use default duration if video not loaded | |
| console.log("Timeline render - duration:", duration); | |
| console.log("Timeline render - player:", this.player); | |
| console.log("Subtitles:", SubtitleManager.getSubtitles()); | |
| const subtitles = SubtitleManager.getSubtitles(); | |
| const maxSubtitleTime = subtitles.length > 0 | |
| ? Math.max(...subtitles.map(s => s.end)) | |
| : duration; | |
| const contentDuration = Math.max(duration, maxSubtitleTime); | |
| // Ensure timeline-content is wide enough to contain all subtitles | |
| const minWidth = Math.max(timeline.offsetWidth, timeline.offsetWidth * this.zoomLevel); | |
| timelineContent.style.width = `${minWidth}px`; | |
| // Render time markers | |
| const baseInterval = 5; // základní interval v sekundách | |
| const interval = Math.max(1, Math.floor(baseInterval / this.zoomLevel)); | |
| for (let time = 0; time <= contentDuration; time += interval) { | |
| const marker = document.createElement('div'); | |
| marker.className = 'timeline-marker'; | |
| marker.style.left = `${(time / contentDuration) * 100}%`; | |
| marker.style.width = '2px'; | |
| const label = document.createElement('div'); | |
| label.textContent = this.formatTime(time); | |
| label.style.position = 'absolute'; | |
| label.style.top = '-20px'; | |
| label.style.transform = 'translateX(-50%)'; | |
| marker.appendChild(label); | |
| timelineContent.appendChild(marker); | |
| } | |
| // Render subtitles | |
| SubtitleManager.getSubtitles().forEach((subtitle, index) => { | |
| const element = document.createElement('div'); | |
| element.className = 'subtitle-marker'; | |
| const leftPosition = (subtitle.start / contentDuration) * 100; | |
| const width = ((subtitle.end - subtitle.start) / contentDuration) * 100; | |
| element.style.left = `${leftPosition}%`; | |
| element.style.width = `${width}%`; | |
| element.textContent = subtitle.text; | |
| // Přidání handlerů pro změnu velikosti | |
| const leftHandle = document.createElement('div'); | |
| leftHandle.className = 'resize-handle left'; | |
| const rightHandle = document.createElement('div'); | |
| rightHandle.className = 'resize-handle right'; | |
| element.appendChild(leftHandle); | |
| element.appendChild(rightHandle); | |
| let isResizing = false; | |
| let startX = 0; | |
| let startLeft = 0; | |
| let startWidth = 0; | |
| let originalStart = 0; | |
| let originalEnd = 0; | |
| const onMouseMove = (e) => { | |
| if (!isResizing) return; | |
| const timeline = document.getElementById('timeline'); | |
| const timelineContent = document.getElementById('timeline-content'); | |
| const rect = timeline.getBoundingClientRect(); | |
| const scrollOffset = timeline.scrollLeft; | |
| const duration = this.player.duration() || 100; | |
| const subtitles = SubtitleManager.getSubtitles(); | |
| const maxSubtitleTime = subtitles.length > 0 | |
| ? Math.max(...subtitles.map(s => s.end)) | |
| : duration; | |
| const contentDuration = Math.max(duration, maxSubtitleTime); | |
| // Calculate mouse position relative to the scrolled content | |
| const mouseX = e.clientX - rect.left + scrollOffset; | |
| const totalWidth = timelineContent.scrollWidth; // Use scrollWidth instead of offsetWidth | |
| const percentage = mouseX / totalWidth; | |
| const newTime = Math.max(0, Math.min(contentDuration, contentDuration * percentage)); | |
| if (isResizing === 'left') { | |
| if (newTime < subtitle.end && newTime >= 0) { | |
| subtitle.start = newTime; | |
| const startPercentage = (subtitle.start / duration) * 100; | |
| const widthPercentage = ((subtitle.end - subtitle.start) / duration) * 100; | |
| element.style.left = `${startPercentage}%`; | |
| element.style.width = `${widthPercentage}%`; | |
| } | |
| } else if (newTime > subtitle.start && newTime <= contentDuration) { | |
| subtitle.end = newTime; | |
| const widthPercentage = ((subtitle.end - subtitle.start) / contentDuration) * 100; | |
| element.style.width = `${widthPercentage}%`; | |
| } | |
| // Update both timeline and subtitle list | |
| SubtitleManager.renderSubtitleList(); | |
| this.render(); | |
| }; | |
| const onMouseUp = () => { | |
| if (isResizing) { | |
| isResizing = false; | |
| document.removeEventListener('mousemove', onMouseMove); | |
| document.removeEventListener('mouseup', onMouseUp); | |
| element.classList.remove('resizing'); | |
| SubtitleManager.renderSubtitleList(); | |
| } | |
| }; | |
| element.addEventListener('mousedown', (e) => { | |
| const handle = e.target.closest('.resize-handle'); | |
| if (handle) { | |
| isResizing = handle.classList.contains('left') ? 'left' : 'right'; | |
| // Zabránit otevření modálního okna a propagaci události | |
| e.stopPropagation(); | |
| e.preventDefault(); | |
| const rect = element.getBoundingClientRect(); | |
| const parentRect = element.parentElement.getBoundingClientRect(); | |
| startX = e.clientX - rect.left + (handle.classList.contains('left') ? 0 : rect.width); | |
| startLeft = subtitle.start / (this.player.duration() || 100) * parentRect.width; | |
| startWidth = rect.width; | |
| originalStart = subtitle.start; | |
| originalEnd = subtitle.end; | |
| document.addEventListener('mousemove', onMouseMove); | |
| document.addEventListener('mouseup', onMouseUp); | |
| // Přidat třídu pro indikaci resize operace | |
| element.classList.add('resizing'); | |
| } | |
| }); | |
| element.addEventListener('click', (e) => { | |
| const timeline = document.getElementById('timeline'); | |
| // Don't open modal if clicking resize handle or during resize | |
| if (e.target.closest('.resize-handle') || element.classList.contains('resizing')) { | |
| return; | |
| } | |
| if (e.ctrlKey || e.metaKey) { | |
| // Ctrl+click to play from subtitle start | |
| const timelineWidth = timeline.offsetWidth; | |
| const duration = this.player.duration() || 100; | |
| const position = (subtitle.start / duration) * timeline.scrollWidth; | |
| // Center the subtitle in view | |
| timeline.scrollLeft = Math.max(0, position - timelineWidth / 2); | |
| // Set video time after ensuring subtitle is in view | |
| requestAnimationFrame(() => { | |
| this.player.currentTime(subtitle.start); | |
| }); | |
| } else { | |
| // Normal click for editing | |
| const modal = new bootstrap.Modal(document.getElementById('editSubtitleModal')); | |
| const textArea = document.getElementById('subtitleText'); | |
| textArea.value = subtitle.text; | |
| const saveButton = document.getElementById('saveSubtitle'); | |
| const deleteButton = document.getElementById('deleteSubtitle'); | |
| const saveHandler = () => { | |
| subtitle.text = textArea.value; | |
| SubtitleManager.renderSubtitleList(); | |
| this.render(); | |
| modal.hide(); | |
| saveButton.removeEventListener('click', saveHandler); | |
| deleteButton.removeEventListener('click', deleteHandler); | |
| }; | |
| const deleteHandler = () => { | |
| SubtitleManager.subtitles.splice(index, 1); | |
| SubtitleManager.renderSubtitleList(); | |
| this.render(); | |
| modal.hide(); | |
| saveButton.removeEventListener('click', saveHandler); | |
| deleteButton.removeEventListener('click', deleteHandler); | |
| }; | |
| saveButton.addEventListener('click', saveHandler); | |
| deleteButton.addEventListener('click', deleteHandler); | |
| modal.show(); | |
| } | |
| }); | |
| timelineContent.appendChild(element); | |
| }); | |
| }, | |
| formatTime: function(seconds) { | |
| const date = new Date(seconds * 1000); | |
| const mm = date.getUTCMinutes(); | |
| const ss = date.getUTCSeconds(); | |
| return `${mm.toString().padStart(2, '0')}:${ss.toString().padStart(2, '0')}`; | |
| }, | |
| updateTimeline: function() { | |
| this.render(); | |
| } | |
| }; | |