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