koala2 / static /js /timeline.js
arcanus's picture
Create timeline.js
ba59f5e verified
raw
history blame
12.6 kB
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();
}
};