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(); } };