mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-10 09:28:53 -05:00
feat: Create the custom icon and chapters sidebar
This commit is contained in:
parent
b3ab626c91
commit
dc9a5492db
@ -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;
|
||||||
@ -0,0 +1,115 @@
|
|||||||
|
/* CustomSettingsMenu.css */
|
||||||
|
|
||||||
|
/* Settings button styling */
|
||||||
|
.vjs-settings-button {
|
||||||
|
width: 3em;
|
||||||
|
height: 3em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings button icon styling */
|
||||||
|
.vjs-icon-cog1 {
|
||||||
|
font-size: 30px !important;
|
||||||
|
position: relative;
|
||||||
|
top: -8px !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings overlay styling */
|
||||||
|
.custom-settings-overlay {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
right: 0;
|
||||||
|
width: 250px;
|
||||||
|
background: rgba(28, 28, 28, 0.95);
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||||
|
display: none;
|
||||||
|
z-index: 1000;
|
||||||
|
font-size: 14px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings header */
|
||||||
|
.settings-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings items */
|
||||||
|
.settings-item {
|
||||||
|
padding: 12px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Speed submenu */
|
||||||
|
.speed-submenu {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(28, 28, 28, 0.95);
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Submenu header */
|
||||||
|
.submenu-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-header:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Speed options */
|
||||||
|
.speed-option {
|
||||||
|
padding: 12px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speed-option:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.speed-option.active {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
.vjs-icon-cog:before {
|
||||||
|
font-size: 20px !important;
|
||||||
|
position: relative;
|
||||||
|
top: -5px !important;
|
||||||
|
}
|
||||||
@ -0,0 +1,242 @@
|
|||||||
|
// components/controls/CustomSettingsMenu.js
|
||||||
|
import videojs from 'video.js';
|
||||||
|
import './CustomSettingsMenu.css';
|
||||||
|
|
||||||
|
// Get the Component base class from Video.js
|
||||||
|
const Component = videojs.getComponent('Component');
|
||||||
|
|
||||||
|
class CustomSettingsMenu extends Component {
|
||||||
|
constructor(player, options) {
|
||||||
|
super(player, options);
|
||||||
|
|
||||||
|
this.settingsButton = null;
|
||||||
|
this.settingsOverlay = null;
|
||||||
|
this.speedSubmenu = null;
|
||||||
|
|
||||||
|
// Bind methods
|
||||||
|
this.createSettingsButton = this.createSettingsButton.bind(this);
|
||||||
|
this.createSettingsOverlay = this.createSettingsOverlay.bind(this);
|
||||||
|
this.positionButton = this.positionButton.bind(this);
|
||||||
|
this.toggleSettings = this.toggleSettings.bind(this);
|
||||||
|
this.handleSpeedChange = this.handleSpeedChange.bind(this);
|
||||||
|
this.handleClickOutside = this.handleClickOutside.bind(this);
|
||||||
|
|
||||||
|
// Initialize after player is ready
|
||||||
|
this.player().ready(() => {
|
||||||
|
this.createSettingsButton();
|
||||||
|
this.createSettingsOverlay();
|
||||||
|
this.setupEventListeners();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createSettingsButton() {
|
||||||
|
const controlBar = this.player().getChild('controlBar');
|
||||||
|
|
||||||
|
// Hide default playback rate button
|
||||||
|
const playbackRateButton = controlBar.getChild('playbackRateMenuButton');
|
||||||
|
if (playbackRateButton) {
|
||||||
|
playbackRateButton.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create settings button
|
||||||
|
this.settingsButton = controlBar.addChild('button', {
|
||||||
|
controlText: 'Settings',
|
||||||
|
className: 'vjs-settings-button',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Style the settings button (gear icon)
|
||||||
|
const settingsButtonEl = this.settingsButton.el();
|
||||||
|
settingsButtonEl.innerHTML = `
|
||||||
|
<span class="vjs-icon-cog"></span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Position the settings button at the end of the control bar
|
||||||
|
this.positionButton();
|
||||||
|
|
||||||
|
// Add click handler
|
||||||
|
this.settingsButton.on('click', this.toggleSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
createSettingsOverlay() {
|
||||||
|
const controlBar = this.player().getChild('controlBar');
|
||||||
|
|
||||||
|
// Create settings overlay
|
||||||
|
this.settingsOverlay = document.createElement('div');
|
||||||
|
this.settingsOverlay.className = 'custom-settings-overlay';
|
||||||
|
|
||||||
|
// Settings menu content
|
||||||
|
this.settingsOverlay.innerHTML = `
|
||||||
|
<div class="settings-header">Settings</div>
|
||||||
|
|
||||||
|
<div class="settings-item" data-setting="playback-speed">
|
||||||
|
<span>Playback speed</span>
|
||||||
|
<span class="current-speed">Normal</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-item" data-setting="quality">
|
||||||
|
<span>Quality</span>
|
||||||
|
<span class="current-quality">Auto</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create speed submenu
|
||||||
|
this.createSpeedSubmenu();
|
||||||
|
|
||||||
|
// Add to control bar
|
||||||
|
controlBar.el().appendChild(this.settingsOverlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
createSpeedSubmenu() {
|
||||||
|
const speedOptions = [
|
||||||
|
{ label: '0.25', value: 0.25 },
|
||||||
|
{ label: '0.5', value: 0.5 },
|
||||||
|
{ label: '0.75', value: 0.75 },
|
||||||
|
{ label: 'Normal', value: 1 },
|
||||||
|
{ label: '1.25', value: 1.25 },
|
||||||
|
{ label: '1.5', value: 1.5 },
|
||||||
|
{ label: '1.75', value: 1.75 },
|
||||||
|
{ label: '2', value: 2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
this.speedSubmenu = document.createElement('div');
|
||||||
|
this.speedSubmenu.className = 'speed-submenu';
|
||||||
|
|
||||||
|
this.speedSubmenu.innerHTML = `
|
||||||
|
<div class="submenu-header">
|
||||||
|
<span style="margin-right: 8px;">←</span>
|
||||||
|
<span>Playback speed</span>
|
||||||
|
</div>
|
||||||
|
${speedOptions
|
||||||
|
.map(
|
||||||
|
(option) => `
|
||||||
|
<div class="speed-option ${option.value === 1 ? 'active' : ''}" data-speed="${option.value}">
|
||||||
|
<span>${option.label}</span>
|
||||||
|
${option.value === 1 ? '<span>✓</span>' : ''}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join('')}
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.settingsOverlay.appendChild(this.speedSubmenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
positionButton() {
|
||||||
|
const controlBar = this.player().getChild('controlBar');
|
||||||
|
const fullscreenToggle = controlBar.getChild('fullscreenToggle');
|
||||||
|
|
||||||
|
if (this.settingsButton && fullscreenToggle) {
|
||||||
|
// Small delay to ensure all buttons are created
|
||||||
|
setTimeout(() => {
|
||||||
|
const fullscreenIndex = controlBar.children().indexOf(fullscreenToggle);
|
||||||
|
controlBar.removeChild(this.settingsButton);
|
||||||
|
controlBar.addChild(this.settingsButton, {}, fullscreenIndex + 1);
|
||||||
|
console.log('✓ Settings button positioned after fullscreen toggle');
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// Settings item clicks
|
||||||
|
this.settingsOverlay.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (e.target.closest('[data-setting="playback-speed"]')) {
|
||||||
|
this.speedSubmenu.style.display = 'flex';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Speed submenu header (back button)
|
||||||
|
this.speedSubmenu.querySelector('.submenu-header').addEventListener('click', () => {
|
||||||
|
this.speedSubmenu.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Speed option clicks
|
||||||
|
this.speedSubmenu.addEventListener('click', (e) => {
|
||||||
|
const speedOption = e.target.closest('.speed-option');
|
||||||
|
if (speedOption) {
|
||||||
|
const speed = parseFloat(speedOption.dataset.speed);
|
||||||
|
this.handleSpeedChange(speed, speedOption);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close menu when clicking outside
|
||||||
|
document.addEventListener('click', this.handleClickOutside);
|
||||||
|
|
||||||
|
// Add hover effects
|
||||||
|
this.settingsOverlay.addEventListener('mouseover', (e) => {
|
||||||
|
const item = e.target.closest('.settings-item, .speed-option');
|
||||||
|
if (item && !item.style.background.includes('0.1')) {
|
||||||
|
item.style.background = 'rgba(255, 255, 255, 0.05)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.settingsOverlay.addEventListener('mouseout', (e) => {
|
||||||
|
const item = e.target.closest('.settings-item, .speed-option');
|
||||||
|
if (item && !item.style.background.includes('0.1')) {
|
||||||
|
item.style.background = 'transparent';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSettings(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const isVisible = this.settingsOverlay.style.display === 'block';
|
||||||
|
this.settingsOverlay.style.display = isVisible ? 'none' : 'block';
|
||||||
|
this.speedSubmenu.style.display = 'none'; // Hide submenu when main menu toggles
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSpeedChange(speed, speedOption) {
|
||||||
|
// Update player speed
|
||||||
|
this.player().playbackRate(speed);
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
document.querySelectorAll('.speed-option').forEach((opt) => {
|
||||||
|
opt.style.background = 'transparent';
|
||||||
|
opt.querySelector('span:last-child')?.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
speedOption.style.background = 'rgba(255, 255, 255, 0.1)';
|
||||||
|
speedOption.insertAdjacentHTML('beforeend', '<span>✓</span>');
|
||||||
|
|
||||||
|
// Update main menu display
|
||||||
|
const currentSpeedDisplay = this.settingsOverlay.querySelector('.current-speed');
|
||||||
|
currentSpeedDisplay.textContent = speedOption.querySelector('span').textContent;
|
||||||
|
|
||||||
|
// Hide menus
|
||||||
|
this.settingsOverlay.style.display = 'none';
|
||||||
|
this.speedSubmenu.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickOutside(e) {
|
||||||
|
if (
|
||||||
|
this.settingsOverlay &&
|
||||||
|
this.settingsButton &&
|
||||||
|
!this.settingsOverlay.contains(e.target) &&
|
||||||
|
!this.settingsButton.el().contains(e.target)
|
||||||
|
) {
|
||||||
|
this.settingsOverlay.style.display = 'none';
|
||||||
|
this.speedSubmenu.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
// Remove event listeners
|
||||||
|
document.removeEventListener('click', this.handleClickOutside);
|
||||||
|
|
||||||
|
// Remove DOM elements
|
||||||
|
if (this.settingsOverlay) {
|
||||||
|
this.settingsOverlay.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set component name for Video.js
|
||||||
|
CustomSettingsMenu.prototype.controlText_ = 'Settings Menu';
|
||||||
|
|
||||||
|
// Register the component with Video.js
|
||||||
|
videojs.registerComponent('CustomSettingsMenu', CustomSettingsMenu);
|
||||||
|
|
||||||
|
export default CustomSettingsMenu;
|
||||||
@ -7,6 +7,8 @@ import EndScreenOverlay from '../overlays/EndScreenOverlay';
|
|||||||
import ChapterMarkers from '../markers/ChapterMarkers';
|
import ChapterMarkers from '../markers/ChapterMarkers';
|
||||||
import NextVideoButton from '../controls/NextVideoButton';
|
import NextVideoButton from '../controls/NextVideoButton';
|
||||||
import CustomRemainingTime from '../controls/CustomRemainingTime';
|
import CustomRemainingTime from '../controls/CustomRemainingTime';
|
||||||
|
import CustomChaptersOverlay from '../controls/CustomChaptersOverlay';
|
||||||
|
import CustomSettingsMenu from '../controls/CustomSettingsMenu';
|
||||||
|
|
||||||
function VideoJSPlayer() {
|
function VideoJSPlayer() {
|
||||||
const videoRef = useRef(null);
|
const videoRef = useRef(null);
|
||||||
@ -366,7 +368,7 @@ function VideoJSPlayer() {
|
|||||||
descriptionsButton: true,
|
descriptionsButton: true,
|
||||||
|
|
||||||
// Subtitles button
|
// Subtitles button
|
||||||
subtitlesButton: true,
|
subtitlesButton: false,
|
||||||
|
|
||||||
// Captions button (disabled to avoid duplicate)
|
// Captions button (disabled to avoid duplicate)
|
||||||
captionsButton: false,
|
captionsButton: false,
|
||||||
@ -421,7 +423,19 @@ function VideoJSPlayer() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Event listeners
|
// Event listeners
|
||||||
playerRef.current.on('ready', () => {
|
/* playerRef.current.on('ready', () => {
|
||||||
|
console.log('Video.js player ready');
|
||||||
|
}); */
|
||||||
|
playerRef.current.ready(() => {
|
||||||
|
// Get control bar and its children
|
||||||
|
const controlBar = playerRef.current.getChild('controlBar');
|
||||||
|
const playToggle = controlBar.getChild('playToggle');
|
||||||
|
const currentTimeDisplay = controlBar.getChild('currentTimeDisplay');
|
||||||
|
const progressControl = controlBar.getChild('progressControl');
|
||||||
|
const seekBar = progressControl.getChild('seekBar');
|
||||||
|
const chaptersButton = controlBar.getChild('chaptersButton');
|
||||||
|
const fullscreenToggle = controlBar.getChild('fullscreenToggle');
|
||||||
|
|
||||||
// Auto-play video when navigating from next button
|
// Auto-play video when navigating from next button
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const hasVideoParam = urlParams.get('m');
|
const hasVideoParam = urlParams.get('m');
|
||||||
@ -436,8 +450,8 @@ function VideoJSPlayer() {
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add English subtitle track after player is ready
|
// BEGIN: Add subtitle tracks
|
||||||
playerRef.current.addRemoteTextTrack(
|
const subtitleTracks = [
|
||||||
{
|
{
|
||||||
kind: 'subtitles',
|
kind: 'subtitles',
|
||||||
src: '/sample-subtitles.vtt',
|
src: '/sample-subtitles.vtt',
|
||||||
@ -445,11 +459,6 @@ function VideoJSPlayer() {
|
|||||||
label: 'English Subtitles',
|
label: 'English Subtitles',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add Greek subtitle track
|
|
||||||
playerRef.current.addRemoteTextTrack(
|
|
||||||
{
|
{
|
||||||
kind: 'subtitles',
|
kind: 'subtitles',
|
||||||
src: '/sample-subtitles-greek.vtt',
|
src: '/sample-subtitles-greek.vtt',
|
||||||
@ -457,12 +466,16 @@ function VideoJSPlayer() {
|
|||||||
label: 'Greek Subtitles (Ελληνικά)',
|
label: 'Greek Subtitles (Ελληνικά)',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
false
|
];
|
||||||
);
|
|
||||||
|
|
||||||
// Create a text track for chapters programmatically
|
subtitleTracks.forEach((track) => {
|
||||||
|
playerRef.current.addRemoteTextTrack(track, false);
|
||||||
|
});
|
||||||
|
// END: Add subtitle tracks
|
||||||
|
|
||||||
|
// BEGIN: Chapters Implementation
|
||||||
|
if (chaptersData && chaptersData.length > 0) {
|
||||||
const chaptersTrack = playerRef.current.addTextTrack('chapters', 'Chapters', 'en');
|
const chaptersTrack = playerRef.current.addTextTrack('chapters', 'Chapters', 'en');
|
||||||
|
|
||||||
// Add cues to the chapters track
|
// Add cues to the chapters track
|
||||||
chaptersData.forEach((chapter) => {
|
chaptersData.forEach((chapter) => {
|
||||||
const cue = new (window.VTTCue || window.TextTrackCue)(
|
const cue = new (window.VTTCue || window.TextTrackCue)(
|
||||||
@ -472,31 +485,15 @@ function VideoJSPlayer() {
|
|||||||
);
|
);
|
||||||
chaptersTrack.addCue(cue);
|
chaptersTrack.addCue(cue);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
// END: Chapters Implementation
|
||||||
|
|
||||||
// Force chapter markers update after chapters are loaded
|
// Force chapter markers update after chapters are loaded
|
||||||
setTimeout(() => {
|
/* setTimeout(() => {
|
||||||
const progressControl = playerRef.current
|
if (chapterMarkers && chapterMarkers.updateChapterMarkers) {
|
||||||
.getChild('controlBar')
|
chapterMarkers.updateChapterMarkers();
|
||||||
.getChild('progressControl');
|
|
||||||
if (progressControl) {
|
|
||||||
const seekBar = progressControl.getChild('seekBar');
|
|
||||||
if (seekBar) {
|
|
||||||
const markers = seekBar.getChild('ChapterMarkers');
|
|
||||||
if (markers && markers.updateChapterMarkers) {
|
|
||||||
markers.updateChapterMarkers();
|
|
||||||
}
|
}
|
||||||
}
|
}, 500); */
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
console.log('Subtitles loaded but disabled by default - use CC button to enable');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add Next Video button to control bar and reorder chapters button
|
|
||||||
playerRef.current.ready(() => {
|
|
||||||
const controlBar = playerRef.current.getChild('controlBar');
|
|
||||||
const playToggle = controlBar.getChild('playToggle');
|
|
||||||
const currentTimeDisplay = controlBar.getChild('currentTimeDisplay');
|
|
||||||
|
|
||||||
// BEGIN: Implement custom time display component
|
// BEGIN: Implement custom time display component
|
||||||
const customRemainingTime = new CustomRemainingTime(playerRef.current, {
|
const customRemainingTime = new CustomRemainingTime(playerRef.current, {
|
||||||
@ -523,7 +520,7 @@ function VideoJSPlayer() {
|
|||||||
// END: Implement custom next video button
|
// END: Implement custom next video button
|
||||||
|
|
||||||
// Remove duplicate captions button and move chapters to end
|
// Remove duplicate captions button and move chapters to end
|
||||||
const cleanupControls = () => {
|
/* const cleanupControls = () => {
|
||||||
// Log all current children for debugging
|
// Log all current children for debugging
|
||||||
const allChildren = controlBar.children();
|
const allChildren = controlBar.children();
|
||||||
|
|
||||||
@ -561,12 +558,12 @@ function VideoJSPlayer() {
|
|||||||
console.log('✗ Failed to move chapters button:', e);
|
console.log('✗ Failed to move chapters button:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}; */
|
||||||
|
|
||||||
// Try multiple times with different delays
|
// Try multiple times with different delays
|
||||||
setTimeout(cleanupControls, 200);
|
/* setTimeout(cleanupControls, 200);
|
||||||
setTimeout(cleanupControls, 500);
|
setTimeout(cleanupControls, 500);
|
||||||
setTimeout(cleanupControls, 1000);
|
setTimeout(cleanupControls, 1000); */
|
||||||
|
|
||||||
// Make menus clickable instead of hover-only
|
// Make menus clickable instead of hover-only
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -628,15 +625,46 @@ function VideoJSPlayer() {
|
|||||||
setupClickableMenus();
|
setupClickableMenus();
|
||||||
}, 1500);
|
}, 1500);
|
||||||
|
|
||||||
// Add chapter markers to progress control
|
// BEGIN: Add chapter markers to progress control
|
||||||
const progressControl = controlBar.getChild('progressControl');
|
if (progressControl && seekBar) {
|
||||||
if (progressControl) {
|
|
||||||
const progressHolder = progressControl.getChild('seekBar');
|
|
||||||
if (progressHolder) {
|
|
||||||
const chapterMarkers = new ChapterMarkers(playerRef.current);
|
const chapterMarkers = new ChapterMarkers(playerRef.current);
|
||||||
progressHolder.addChild(chapterMarkers);
|
seekBar.addChild(chapterMarkers);
|
||||||
|
}
|
||||||
|
// END: Add chapter markers to progress control
|
||||||
|
|
||||||
|
// BEGIN: Move chapters button after fullscreen toggle
|
||||||
|
if (chaptersButton && fullscreenToggle) {
|
||||||
|
try {
|
||||||
|
const fullscreenIndex = controlBar.children().indexOf(fullscreenToggle);
|
||||||
|
controlBar.addChild(chaptersButton, {}, fullscreenIndex + 1);
|
||||||
|
console.log('✓ Chapters button moved after fullscreen toggle');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('✗ Failed to move chapters button:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// END: Move chapters button after fullscreen toggle
|
||||||
|
|
||||||
|
// Store custom components for potential future use (cleanup, method access, etc.)
|
||||||
|
const customComponents = {};
|
||||||
|
|
||||||
|
// BEGIN: Add Chapters Overlay Component
|
||||||
|
if (chaptersData && chaptersData.length > 0) {
|
||||||
|
customComponents.chaptersOverlay = new CustomChaptersOverlay(playerRef.current, {
|
||||||
|
chaptersData: chaptersData,
|
||||||
|
});
|
||||||
|
console.log('✓ Custom chapters overlay component created');
|
||||||
|
} else {
|
||||||
|
console.log('⚠ No chapters data available for overlay');
|
||||||
|
}
|
||||||
|
// END: Add Chapters Overlay Component
|
||||||
|
|
||||||
|
// BEGIN: Add Settings Menu Component
|
||||||
|
customComponents.settingsMenu = new CustomSettingsMenu(playerRef.current);
|
||||||
|
console.log('✓ Custom settings menu component created');
|
||||||
|
// END: Add Settings Menu Component
|
||||||
|
|
||||||
|
// Store components reference for potential cleanup
|
||||||
|
console.log('Custom components initialized:', Object.keys(customComponents));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for next video event
|
// Listen for next video event
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user