feat: Create the custom icon and chapters sidebar

This commit is contained in:
Yiannis Christodoulou
2025-07-14 03:19:27 +03:00
parent b3ab626c91
commit dc9a5492db
4 changed files with 641 additions and 55 deletions
@@ -0,0 +1,201 @@
// components/controls/CustomChaptersOverlay.js
import videojs from 'video.js';
// Get the Component base class from Video.js
const Component = videojs.getComponent('Component');
class CustomChaptersOverlay extends Component {
constructor(player, options) {
super(player, options);
this.chaptersData = options.chaptersData || [];
this.overlay = null;
this.chaptersList = null;
// Bind methods
this.createOverlay = this.createOverlay.bind(this);
this.updateCurrentChapter = this.updateCurrentChapter.bind(this);
this.toggleOverlay = this.toggleOverlay.bind(this);
// Initialize after player is ready
this.player().ready(() => {
this.createOverlay();
this.setupChaptersButton();
});
}
createOverlay() {
if (!this.chaptersData || this.chaptersData.length === 0) {
console.log('⚠ No chapters data available for overlay');
return;
}
const playerEl = this.player().el();
// Create overlay element
this.overlay = document.createElement('div');
this.overlay.className = 'custom-chapters-overlay';
this.overlay.style.cssText = `
position: absolute;
top: 0;
right: 0;
width: 300px;
height: 100%;
background: linear-gradient(180deg, rgba(20, 20, 30, 0.95) 0%, rgba(40, 40, 50, 0.95) 100%);
color: white;
z-index: 1000;
display: none;
overflow-y: auto;
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.5);
`;
// Create header
const header = document.createElement('div');
header.style.cssText = `
background: rgba(0, 0, 0, 0.8);
padding: 20px;
text-align: center;
font-weight: bold;
font-size: 14px;
letter-spacing: 2px;
border-bottom: 2px solid #4a90e2;
position: sticky;
top: 0;
`;
header.textContent = 'CHAPTERS';
this.overlay.appendChild(header);
// Create close button
const closeBtn = document.createElement('div');
closeBtn.style.cssText = `
position: absolute;
top: 15px;
right: 15px;
width: 25px;
height: 25px;
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 16px;
z-index: 10;
`;
closeBtn.textContent = '×';
closeBtn.onclick = () => {
this.overlay.style.display = 'none';
};
this.overlay.appendChild(closeBtn);
// Create chapters list
this.chaptersList = document.createElement('div');
this.chaptersList.style.cssText = `
padding: 10px 0;
`;
// Add chapters from data
this.chaptersData.forEach((chapter) => {
const chapterItem = document.createElement('div');
chapterItem.style.cssText = `
padding: 15px 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
transition: background 0.2s ease;
font-size: 14px;
line-height: 1.4;
`;
chapterItem.textContent = chapter.text;
// Add hover effect
chapterItem.onmouseenter = () => {
chapterItem.style.background = 'rgba(74, 144, 226, 0.2)';
};
chapterItem.onmouseleave = () => {
chapterItem.style.background = 'transparent';
};
// Add click handler
chapterItem.onclick = () => {
this.player().currentTime(chapter.startTime);
this.overlay.style.display = 'none';
// Update active state
this.chaptersList.querySelectorAll('div').forEach((item) => {
item.style.background = 'transparent';
item.style.fontWeight = 'normal';
});
chapterItem.style.background = 'rgba(74, 144, 226, 0.4)';
chapterItem.style.fontWeight = 'bold';
};
this.chaptersList.appendChild(chapterItem);
});
this.overlay.appendChild(this.chaptersList);
// Add to player
playerEl.appendChild(this.overlay);
// Set up time update listener
this.player().on('timeupdate', this.updateCurrentChapter);
console.log('✓ Custom chapters overlay created');
}
setupChaptersButton() {
const chaptersButton = this.player().getChild('controlBar').getChild('chaptersButton');
if (chaptersButton) {
// Override the click handler
chaptersButton.off('click'); // Remove default handler
chaptersButton.on('click', this.toggleOverlay);
}
}
toggleOverlay() {
if (!this.overlay) return;
if (this.overlay.style.display === 'none' || !this.overlay.style.display) {
this.overlay.style.display = 'block';
} else {
this.overlay.style.display = 'none';
}
}
updateCurrentChapter() {
if (!this.chaptersList || !this.chaptersData) return;
const currentTime = this.player().currentTime();
const chapterItems = this.chaptersList.querySelectorAll('div');
chapterItems.forEach((item, index) => {
const chapter = this.chaptersData[index];
const isPlaying =
currentTime >= chapter.startTime &&
(index === this.chaptersData.length - 1 || currentTime < this.chaptersData[index + 1].startTime);
if (isPlaying) {
item.style.borderLeft = '4px solid #10b981';
item.style.paddingLeft = '16px';
} else {
item.style.borderLeft = 'none';
item.style.paddingLeft = '20px';
}
});
}
dispose() {
if (this.overlay) {
this.overlay.remove();
}
super.dispose();
}
}
// Set component name for Video.js
CustomChaptersOverlay.prototype.controlText_ = 'Chapters Overlay';
// Register the component with Video.js
videojs.registerComponent('CustomChaptersOverlay', CustomChaptersOverlay);
export default CustomChaptersOverlay;