fix: Video.js improve styling of Toolbar icons

- chapter popup
- settings popup
- autoplay
-align the icons properly in the bar
This commit is contained in:
Yiannis Christodoulou 2025-09-10 13:53:37 +03:00
parent d88f4a27cc
commit 3accbd29ce
8 changed files with 1559 additions and 850 deletions

View File

@ -71,10 +71,10 @@
transition: all 0.2s ease !important;
}
.vjs-autoplay-toggle:hover {
/* .vjs-autoplay-toggle:hover {
color: #ff4444 !important;
transform: scale(1.1) !important;
}
} */
.vjs-autoplay-toggle .vjs-autoplay-icon {
width: 1.2em;
@ -360,6 +360,17 @@
visibility: visible !important;
}
/* Hide menus/tooltips when chapters overlay is open */
.video-js.chapters-open .vjs-menu,
.video-js.chapters-open .vjs-menu.vjs-lock-showing,
.video-js.chapters-open .vjs-hover-display,
.video-js.chapters-open .vjs-time-tooltip,
.video-js.chapters-open .vjs-progress-holder .vjs-mouse-display {
display: none !important;
opacity: 0 !important;
visibility: hidden !important;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.video-container {
@ -681,3 +692,55 @@
text-align: center !important;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8) !important;
}
button{cursor: pointer;}
body{ font-family: Arial, Helvetica, sans-serif; box-sizing:border-box;}
.video-wrapper{ position:relative; font-family:Arial; height:calc(100vh - 16px);}
.video-box{ height:100%;}
.video-wrapper .video-box .video-js{ padding:0; height:100% !important;}
.video-chapter{ position:absolute; top:10px; width:min(360px, calc(100% - 20px)); border:1px solid rgba(255, 255, 255, 0.12); border-radius:12px;
height:calc(100% - 80px); background:rgba(18, 18, 18, 0.96); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); overflow:hidden;
box-shadow: 0 12px 30px rgba(0,0,0,0.45); right:10px;}
.chapter-head{ padding:12px 8px 10px 16px; position:sticky; top:0; left:0; background:linear-gradient(180deg, rgba(28,28,28,0.95), rgba(18,18,18,0.95));
border-bottom:1px solid rgba(255,255,255,0.08); z-index:2; }
.playlist-title{ display:flex; align-items:center; gap:10px; }
.chapter-title{ width: auto; flex: 1; min-width: 0; }
.chapter-title h3{ margin:0; padding:0;}
.chapter-title h3 a{color:#fff; font-size: 18px; line-height: 26px; font-weight: 700; text-decoration: none;
white-space: nowrap; text-overflow: ellipsis; height: 28px; overflow: hidden; display: block;}
.chapter-title p{ margin:4px 0 0; padding:0; color:#bdbdbd; font-size:12px; font-weight:400; line-height:15px;}
.chapter-title p a{ color:#bdbdbd; font-size:12px; font-weight:400; line-height:15px; text-decoration: none;}
.chapter-close{ width:40px; margin-left:auto; display:flex; align-items:center; justify-content:flex-end;}
.chapter-close button{ background:transparent; color:#fff; border: 0; width: 40px; height: 40px; padding: 0; display: flex;
align-items: center; justify-content: center; border-radius:8px;}
.chapter-close button:hover{ background:rgba(255,255,255,0.1);}
.playlist-action-menu{ display:none; justify-content:space-between; gap:10px;}
.playlist-action-menu button{ background:transparent; border: 0; width: 40px; height: 40px; padding: 0; display: flex;
align-items: center; justify-content: center; align-items:center; border-radius:100px; }
.playlist-action-menu button:hover{ background:rgba(0, 0, 0, 0.1);}
.start-action{ display:flex;}
.chapter-body{ height:calc(100% - 59px); overflow:auto; }
.chapter-body ul{ margin:0; padding:0;}
.playlist-items a{ padding:12px; display:flex; align-items:center; text-decoration:none; gap:12px; width:100%; box-sizing:border-box;}
.playlist-items a:hover{background:rgba(255,255,255,0.06);}
.playlist-items.selected a{ background:rgba(255,255,255,0.14);}
.playlist-drag-handle{ width:24px; display:flex; justify-content:center; color:#e0e0e0; font-size:12px;}
.thumbnail-meta{ flex:1; min-width:0; padding:0;}
.thumbnail-meta h4{ margin:0 2px 4px 0; font-size:14px; line-height:20px; font-weight:600; overflow:hidden; text-overflow:ellipsis;
color:#fff; white-space:normal; max-height:40px; -webkit-line-clamp: 2; line-clamp: 2; display: -webkit-box; -webkit-box-orient: vertical;}
.thumbnail-meta .meta-sub{ display:flex; gap:8px; align-items:center; }
.thumbnail-meta .meta-sub .meta-dynamic{ color:#bdbdbd; font-size:12px; line-height:18px; }
.thumbnail-action button{ border:0; background:transparent; color:#fff; opacity:0;}
.playlist-items a:hover .thumbnail-action button{ opacity:1;}
/* custom scrollbar for chapter list */
.chapter-body::-webkit-scrollbar{ width:10px; }
.chapter-body::-webkit-scrollbar-thumb{ background:rgba(255,255,255,0.18); border-radius:8px; }
.chapter-body::-webkit-scrollbar-track{ background:transparent; }
.video-box .video-js .vjs-control-bar .vjs-spacer-control{ margin-left:auto;}
.video-js .vjs-control-bar .settings-item-svg{ display:flex;}
.video-js .vjs-control-bar .settings-item-svg svg{width:auto !important; height: auto !important; transform:inherit !important;}

View File

@ -0,0 +1,156 @@
import React from 'react';
function ChapterList() {
const playlistData = [
{
id: 1,
title: "Class 12 Chapter 1 || Electric Charges and Fields 01 || Quantisation and Conservation of Charge",
channel: "Physics Wallah - Alakh Pandey",
duration: "40:13",
thumbnail: "https://i.ytimg.com/vi/m5VbK66a254/hqdefault.jpg?sqp=-oaymwEmCKgBEF5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLCt2rMJW2jZAYkcDi9wLQOGVkLSTw",
selected: true
},
{
id: 2,
title: "Class 12 Chapter 1 || Electric Charges and Fields 01 || Quantisation and Conservation of Charge",
channel: "Physics Wallah - Alakh Pandey",
duration: "40:13",
thumbnail: "https://i.ytimg.com/vi/m5VbK66a254/hqdefault.jpg?sqp=-oaymwEmCKgBEF5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLCt2rMJW2jZAYkcDi9wLQOGVkLSTw",
selected: false
},
{
id: 3,
title: "Class 12 Chapter 1 || Electric Charges and Fields 01 || Quantisation and Conservation of Charge",
channel: "Physics Wallah - Alakh Pandey",
duration: "40:13",
thumbnail: "https://i.ytimg.com/vi/m5VbK66a254/hqdefault.jpg?sqp=-oaymwEmCKgBEF5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLCt2rMJW2jZAYkcDi9wLQOGVkLSTw",
selected: false
},
{
id: 4,
title: "Class 12 Chapter 1 || Electric Charges and Fields 01 || Quantisation and Conservation of Charge",
channel: "Physics Wallah - Alakh Pandey",
duration: "40:13",
thumbnail: "https://i.ytimg.com/vi/m5VbK66a254/hqdefault.jpg?sqp=-oaymwEmCKgBEF5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLCt2rMJW2jZAYkcDi9wLQOGVkLSTw",
selected: false
},
{
id: 5,
title: "Class 12 Chapter 1 || Electric Charges and Fields 01 || Quantisation and Conservation of Charge",
channel: "Physics Wallah - Alakh Pandey",
duration: "40:13",
thumbnail: "https://i.ytimg.com/vi/m5VbK66a254/hqdefault.jpg?sqp=-oaymwEmCKgBEF5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLCt2rMJW2jZAYkcDi9wLQOGVkLSTw",
selected: false
},
{
id: 6,
title: "Class 12 Chapter 1 || Electric Charges and Fields 01 || Quantisation and Conservation of Charge",
channel: "Physics Wallah - Alakh Pandey",
duration: "40:13",
thumbnail: "https://i.ytimg.com/vi/m5VbK66a254/hqdefault.jpg?sqp=-oaymwEmCKgBEF5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLCt2rMJW2jZAYkcDi9wLQOGVkLSTw",
selected: false
}
,
{
id: 7,
title: "Class 12 Chapter 1 || Electric Charges and Fields 01 || Quantisation and Conservation of Charge",
channel: "Physics Wallah - Alakh Pandey",
duration: "40:13",
thumbnail: "https://i.ytimg.com/vi/m5VbK66a254/hqdefault.jpg?sqp=-oaymwEmCKgBEF5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLCt2rMJW2jZAYkcDi9wLQOGVkLSTw",
selected: false
}
,
{
id: 8,
title: "Class 12 Chapter 1 || Electric Charges and Fields 01 || Quantisation and Conservation of Charge",
channel: "Physics Wallah - Alakh Pandey",
duration: "40:13",
thumbnail: "https://i.ytimg.com/vi/m5VbK66a254/hqdefault.jpg?sqp=-oaymwEmCKgBEF5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLCt2rMJW2jZAYkcDi9wLQOGVkLSTw",
selected: false
}
,
{
id: 9,
title: "Class 12 Chapter 1 || Electric Charges and Fields 01 || Quantisation and Conservation of Charge",
channel: "Physics Wallah - Alakh Pandey",
duration: "40:13",
thumbnail: "https://i.ytimg.com/vi/m5VbK66a254/hqdefault.jpg?sqp=-oaymwEmCKgBEF5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLCt2rMJW2jZAYkcDi9wLQOGVkLSTw",
selected: false
}
];
return (
<div className='video-chapter'>
<div className='chapter-head'>
<div className='playlist-title'>
<div className='chapter-title'>
<h3><a href=''>12 chapter 1 II Electri charges and Fields JEE MAINS/NEET</a></h3>
<p><a href=''>Physics Wallah - Alakh Pandey</a> <span>1 / 17</span></p>
</div>
<div className='chapter-close'>
<button>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.7096 12L20.8596 20.15L20.1496 20.86L11.9996 12.71L3.84965 20.86L3.13965 20.15L11.2896 12L3.14965 3.85001L3.85965 3.14001L11.9996 11.29L20.1496 3.14001L20.8596 3.85001L12.7096 12Z" fill="black"/>
</svg>
</button>
</div>
</div>
<div className='playlist-action-menu'>
<div className='start-action'>
<button>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.0002 13H22.0002V18L3.93023 18.03L6.55023 20.65L5.84023 21.36L1.99023 17.5L5.84023 13.65L6.55023 14.36L3.88023 17.03L21.0002 17V13ZM3.00023 7.00002L20.1202 6.97002L17.4502 9.64002L18.1602 10.35L22.0102 6.50002L18.1602 2.65002L17.4502 3.36002L20.0702 5.98002L2.00023 6.00002V11H3.00023V7.00002Z" fill="black"/>
</svg>
</button>
<button>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.15 13.65L22 17.5L18.15 21.35L17.44 20.64L20.09 18H19C16.16 18 13.47 16.77 11.61 14.62L12.37 13.97C14.03 15.89 16.45 17 19 17H20.09L17.44 14.35L18.15 13.65ZM19 7.00003H20.09L17.44 9.65003L18.15 10.36L22 6.51003L18.15 2.66003L17.44 3.37003L20.09 6.00003H19C15.42 6.00003 12.14 7.95003 10.43 11.09L9.7 12.43C8.16 15.25 5.21 17 2 17V18C5.58 18 8.86 16.05 10.57 12.91L11.3 11.57C12.84 8.75003 15.79 7.00003 19 7.00003ZM8.59 9.98003L9.34 9.32003C7.49 7.21003 4.81 6.00003 2 6.00003V7.00003C4.52 7.00003 6.92 8.09003 8.59 9.98003Z" fill="black"/>
</svg>
</button>
</div>
<div className='end-action'>
<button>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 16.5C12.83 16.5 13.5 17.17 13.5 18C13.5 18.83 12.83 19.5 12 19.5C11.17 19.5 10.5 18.83 10.5 18C10.5 17.17 11.17 16.5 12 16.5ZM10.5 12C10.5 12.83 11.17 13.5 12 13.5C12.83 13.5 13.5 12.83 13.5 12C13.5 11.17 12.83 10.5 12 10.5C11.17 10.5 10.5 11.17 10.5 12ZM10.5 6C10.5 6.83 11.17 7.5 12 7.5C12.83 7.5 13.5 6.83 13.5 6C13.5 5.17 12.83 4.5 12 4.5C11.17 4.5 10.5 5.17 10.5 6Z" fill="black"/>
</svg>
</button>
</div>
</div>
</div>
<div className="chapter-body">
<ul>
{playlistData.map((item) => (
<li key={item.id}>
<div className={`playlist-items ${item.selected ? 'selected' : ''}`}>
<a href="#">
<div className="playlist-drag-handle">{item.selected ? '▶' : item.id}</div>
<div className="thumbnail-container">
<img src={item.thumbnail} alt={item.title} />
<span>{item.duration}</span>
</div>
<div className="thumbnail-meta">
<h4>{item.title}</h4>
<span>{item.channel}</span>
</div>
<div className="thumbnail-action">
<button>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M12 16.5C12.83 16.5 13.5 17.17 13.5 18C13.5 18.83 12.83 19.5 12 19.5C11.17 19.5 10.5 18.83 10.5 18C10.5 17.17 11.17 16.5 12 16.5ZM10.5 12C10.5 12.83 11.17 13.5 12 13.5C12.83 13.5 13.5 12.83 13.5 12C13.5 11.17 12.83 10.5 12 10.5C11.17 10.5 10.5 11.17 10.5 12ZM10.5 6C10.5 6.83 11.17 7.5 12 7.5C12.83 7.5 13.5 6.83 13.5 6C13.5 5.17 12.83 4.5 12 4.5C11.17 4.5 10.5 5.17 10.5 6Z" fill="black"/>
</svg>
</button>
</div>
</a>
</div>
</li>
))}
</ul>
</div>
</div>
);
}
export default ChapterList;

View File

@ -62,7 +62,11 @@ class AutoplayToggleButton extends Button {
updateIcon() {
if (this.isAutoplayEnabled) {
// Simple text icon for now
this.iconSpan.innerHTML = `<span style="font-size: 1.2em; color: #ff4444;">●</span>`;
this.iconSpan.innerHTML = `<span style=" transform: inherit !important; margin: 20px 0 0; font-size: 1.2em; color: #ff4444;"><svg width="198" height="100" viewBox="0 0 198 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="18" y="18" width="180" height="64" rx="32" fill="white"/>
<rect width="100" height="100" rx="50" fill="white"/>
<path d="M53.5714 75V25H75V75H53.5714ZM25 75V25H46.4286V75H25ZM60.7143 67.8571H67.8571V32.1429H60.7143V67.8571ZM32.1429 67.8571H39.2857V32.1429H32.1429V67.8571Z" fill="#1C1B1F"/>
</svg></span>`;
// Only update element properties if element exists
if (this.el()) {
this.el().title = 'Autoplay is on';
@ -74,7 +78,12 @@ class AutoplayToggleButton extends Button {
}
} else {
// Simple text icon for now
this.iconSpan.innerHTML = `<span style="font-size: 1.2em; color: #ccc;">○</span>`;
this.iconSpan.innerHTML = `<span style="transform: inherit !important; margin: 20px 0 0; font-size: 1.2em; color: #ccc;">
<svg width="198" height="100" viewBox="0 0 198 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="18" width="180" height="64" rx="32" fill="white"/>
<rect x="98" width="100" height="100" rx="50" fill="white"/>
<path d="M129 75L168 50L129 25V75ZM136.091 61.9643V38.0357L154.705 50L136.091 61.9643Z" fill="#1C1B1F"/>
</svg></span>`;
// Only update element properties if element exists
if (this.el()) {
this.el().title = 'Autoplay is off';

View File

@ -11,6 +11,9 @@ class CustomChaptersOverlay extends Component {
this.chaptersData = options.chaptersData || [];
this.overlay = null;
this.chaptersList = null;
this.seriesTitle = options.seriesTitle || 'Chapters';
this.channelName = options.channelName || '';
this.thumbnail = options.thumbnail || '';
// Bind methods
this.createOverlay = this.createOverlay.bind(this);
@ -39,101 +42,124 @@ class CustomChaptersOverlay extends Component {
position: absolute;
top: 0;
right: 0;
width: 300px;
width: 100%;
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);
pointer-events: none; /* allow clicks only on inner panel */
background: rgba(0, 0, 0, 0.35);
`;
// Build container
const container = document.createElement('div');
container.className = 'video-chapter';
container.style.pointerEvents = 'auto';
this.overlay.appendChild(container);
// 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.className = 'chapter-head';
container.appendChild(header);
const playlistTitle = document.createElement('div');
playlistTitle.className = 'playlist-title';
header.appendChild(playlistTitle);
const chapterTitle = document.createElement('div');
chapterTitle.className = 'chapter-title';
chapterTitle.innerHTML = `
<h3><a href="#">${this.seriesTitle}</a></h3>
<p><a href="#">${this.channelName}</a> <span>1 / ${this.chaptersData.length}</span></p>
`;
header.textContent = 'CHAPTERS';
this.overlay.appendChild(header);
playlistTitle.appendChild(chapterTitle);
// 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;
const chapterClose = document.createElement('div');
chapterClose.className = 'chapter-close';
const closeBtn = document.createElement('button');
closeBtn.setAttribute('aria-label', 'Close chapters');
closeBtn.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.7096 12L20.8596 20.15L20.1496 20.86L11.9996 12.71L3.84965 20.86L3.13965 20.15L11.2896 12L3.14965 3.85001L3.85965 3.14001L11.9996 11.29L20.1496 3.14001L20.8596 3.85001L12.7096 12Z" fill="currentColor"/>
</svg>
`;
closeBtn.textContent = '×';
closeBtn.onclick = () => {
this.overlay.style.display = 'none';
const el = this.player().el();
if (el) el.classList.remove('chapters-open');
};
this.overlay.appendChild(closeBtn);
chapterClose.appendChild(closeBtn);
playlistTitle.appendChild(chapterClose);
// Create chapters list
this.chaptersList = document.createElement('div');
this.chaptersList.style.cssText = `
padding: 10px 0;
`;
const body = document.createElement('div');
body.className = 'chapter-body';
container.appendChild(body);
const list = document.createElement('ul');
body.appendChild(list);
this.chaptersList = list;
// 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;
this.chaptersData.forEach((chapter, index) => {
const li = document.createElement('li');
const item = document.createElement('div');
item.className = `playlist-items ${index === 0 ? 'selected' : ''}`;
// Add hover effect
chapterItem.onmouseenter = () => {
chapterItem.style.background = 'rgba(74, 144, 226, 0.2)';
};
chapterItem.onmouseleave = () => {
chapterItem.style.background = 'transparent';
};
const anchor = document.createElement('a');
anchor.href = '#';
anchor.onclick = (e) => e.preventDefault();
// Add click handler
chapterItem.onclick = () => {
const drag = document.createElement('div');
drag.className = 'playlist-drag-handle';
drag.textContent = index === 0 ? '▶' : String(index + 1);
const meta = document.createElement('div');
meta.className = 'thumbnail-meta';
// compute duration
const totalSec = Math.max(0, Math.floor((chapter.endTime || chapter.startTime) - chapter.startTime));
const hh = Math.floor(totalSec / 3600);
const mm = Math.floor((totalSec % 3600) / 60);
const ss = totalSec % 60;
const timeStr = hh > 0
? `${String(hh).padStart(2, '0')}:${String(mm).padStart(2, '0')}:${String(ss).padStart(2, '0')}`
: `${String(mm).padStart(2, '0')}:${String(ss).padStart(2, '0')}`;
const titleEl = document.createElement('h4');
titleEl.textContent = chapter.text;
const sub = document.createElement('div');
sub.className = 'meta-sub';
const dynamic = document.createElement('span');
dynamic.className = 'meta-dynamic';
dynamic.textContent = this.channelName;
dynamic.setAttribute('data-duration', timeStr);
sub.appendChild(dynamic);
meta.appendChild(titleEl);
meta.appendChild(sub);
const action = document.createElement('div');
action.className = 'thumbnail-action';
const btn = document.createElement('button');
btn.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 16.5C12.83 16.5 13.5 17.17 13.5 18C13.5 18.83 12.83 19.5 12 19.5C11.17 19.5 10.5 18.83 10.5 18C10.5 17.17 11.17 16.5 12 16.5ZM10.5 12C10.5 12.83 11.17 13.5 12 13.5C12.83 13.5 13.5 12.83 13.5 12C13.5 11.17 12.83 10.5 12 10.5C11.17 10.5 10.5 11.17 10.5 12ZM10.5 6C10.5 6.83 11.17 7.5 12 7.5C12.83 7.5 13.5 6.83 13.5 6C13.5 5.17 12.83 4.5 12 4.5C11.17 4.5 10.5 5.17 10.5 6Z" fill="currentColor"/>
</svg>`;
action.appendChild(btn);
// Click to seek
item.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.updateActiveItem(index);
};
this.chaptersList.appendChild(chapterItem);
anchor.appendChild(drag);
anchor.appendChild(meta);
anchor.appendChild(action);
item.appendChild(anchor);
li.appendChild(item);
this.chaptersList.appendChild(li);
});
this.overlay.appendChild(this.chaptersList);
// Add to player
playerEl.appendChild(this.overlay);
@ -155,10 +181,20 @@ class CustomChaptersOverlay extends Component {
toggleOverlay() {
if (!this.overlay) return;
const el = this.player().el();
if (this.overlay.style.display === 'none' || !this.overlay.style.display) {
this.overlay.style.display = 'block';
if (el) el.classList.add('chapters-open');
// hide any open menus
try {
this.player().el().querySelectorAll('.vjs-menu').forEach((m) => {
m.classList.remove('vjs-lock-showing');
m.style.display = 'none';
});
} catch (e) {}
} else {
this.overlay.style.display = 'none';
if (el) el.classList.remove('chapters-open');
}
}
@ -166,7 +202,7 @@ class CustomChaptersOverlay extends Component {
if (!this.chaptersList || !this.chaptersData) return;
const currentTime = this.player().currentTime();
const chapterItems = this.chaptersList.querySelectorAll('div');
const chapterItems = this.chaptersList.querySelectorAll('.playlist-items');
chapterItems.forEach((item, index) => {
const chapter = this.chaptersData[index];
@ -174,12 +210,33 @@ class CustomChaptersOverlay extends Component {
currentTime >= chapter.startTime &&
(index === this.chaptersData.length - 1 || currentTime < this.chaptersData[index + 1].startTime);
const handle = item.querySelector('.playlist-drag-handle');
const dynamic = item.querySelector('.meta-dynamic');
if (isPlaying) {
item.style.borderLeft = '4px solid #10b981';
item.style.paddingLeft = '16px';
item.classList.add('selected');
if (handle) handle.textContent = '▶';
if (dynamic) dynamic.textContent = dynamic.getAttribute('data-duration') || '';
} else {
item.style.borderLeft = 'none';
item.style.paddingLeft = '20px';
item.classList.remove('selected');
if (handle) handle.textContent = String(index + 1);
if (dynamic) dynamic.textContent = this.channelName;
}
});
}
updateActiveItem(activeIndex) {
const items = this.chaptersList.querySelectorAll('.playlist-items');
items.forEach((el, idx) => {
const handle = el.querySelector('.playlist-drag-handle');
const dynamic = el.querySelector('.meta-dynamic');
if (idx === activeIndex) {
el.classList.add('selected');
if (handle) handle.textContent = '▶';
if (dynamic) dynamic.textContent = dynamic.getAttribute('data-duration') || '';
} else {
el.classList.remove('selected');
if (handle) handle.textContent = String(idx + 1);
if (dynamic) dynamic.textContent = this.channelName;
}
});
}
@ -188,6 +245,8 @@ class CustomChaptersOverlay extends Component {
if (this.overlay) {
this.overlay.remove();
}
const el = this.player().el();
if (el) el.classList.remove('chapters-open');
super.dispose();
}
}

View File

@ -31,7 +31,7 @@
bottom: 60px;
right: 20px;
width: 250px;
height: 600px;
height: 400px;
background: rgba(28, 28, 28, 0.95);
color: white;
border-radius: 7px;
@ -59,6 +59,8 @@
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
transition: background 0.2s ease;
}
.custom-settings-overlay .settings-left span.vjs-icon-placeholder {transform: inherit !important;}
.settings-item:last-child {
border-bottom: none;
@ -80,6 +82,18 @@
flex-direction: column;
}
/* Quality submenu mirrors speed submenu */
.quality-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;
@ -110,8 +124,50 @@
.speed-option.active {
background: rgba(255, 255, 255, 0.1);
}
/* Quality option styling */
.quality-option {
padding: 12px 16px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: background 0.2s ease;
}
.quality-option:hover {
background: rgba(255, 255, 255, 0.05);
}
.quality-option.active {
background: rgba(255, 255, 255, 0.1);
}
/* Settings row left/right layout like YouTube */
.settings-left {
display: inline-flex;
align-items: center;
gap: 8px;
}
.settings-right {
display: inline-flex;
align-items: center;
gap: 8px;
}
.vjs-icon-cog:before {
font-size: 20px !important;
position: relative;
top: -5px !important;
}
/* HD superscript badge for 1080p */
sup.hd-badge {
font-size: 10px;
line-height: 1;
margin-left: 6px;
background: #e53935;
color: #fff;
padding: 1px 4px;
border-radius: 3px;
}

View File

@ -1,263 +1,567 @@
// components/controls/CustomSettingsMenu.js
import videojs from 'video.js';
import './CustomSettingsMenu.css';
import UserPreferences from '../../utils/UserPreferences';
import videojs from "video.js";
import "./CustomSettingsMenu.css";
import UserPreferences from "../../utils/UserPreferences";
// Get the Component base class from Video.js
const Component = videojs.getComponent('Component');
const Component = videojs.getComponent("Component");
class CustomSettingsMenu extends Component {
constructor(player, options) {
super(player, options);
constructor(player, options) {
super(player, options);
this.settingsButton = null;
this.settingsOverlay = null;
this.speedSubmenu = null;
this.userPreferences = options?.userPreferences || new UserPreferences();
this.settingsButton = null;
this.settingsOverlay = null;
this.speedSubmenu = null;
this.qualitySubmenu = null;
this.userPreferences = options?.userPreferences || new UserPreferences();
this.providedQualities = options?.qualities || 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);
// 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.handleQualityChange = this.handleQualityChange.bind(this);
this.getAvailableQualities = this.getAvailableQualities.bind(this);
this.handleClickOutside = this.handleClickOutside.bind(this);
// Initialize after player is ready
this.player().ready(() => {
this.createSettingsButton();
this.createSettingsOverlay();
this.setupEventListeners();
});
}
// Initialize after player is ready
this.player().ready(() => {
this.createSettingsButton();
this.createSettingsOverlay();
this.setupEventListeners();
});
}
createSettingsButton() {
const controlBar = this.player().getChild('controlBar');
createSettingsButton() {
const controlBar = this.player().getChild("controlBar");
// Hide default playback rate button
const playbackRateButton = controlBar.getChild('playbackRateMenuButton');
if (playbackRateButton) {
playbackRateButton.hide();
}
// Do NOT hide default playback rate button to avoid control bar layout shifts
// Create settings button
this.settingsButton = controlBar.addChild('button', {
controlText: 'Settings',
className: 'vjs-settings-button',
});
// 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 = `
// 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();
// Position the settings button at the end of the control bar
this.positionButton();
// Add click handler
this.settingsButton.on('click', this.toggleSettings);
}
// Add click handler
this.settingsButton.on("click", this.toggleSettings);
}
createSettingsOverlay() {
const controlBar = this.player().getChild('controlBar');
createSettingsOverlay() {
const controlBar = this.player().getChild("controlBar");
// Create settings overlay
this.settingsOverlay = document.createElement('div');
this.settingsOverlay.className = 'custom-settings-overlay';
// Create settings overlay
this.settingsOverlay = document.createElement("div");
this.settingsOverlay.className = "custom-settings-overlay";
// Get current preferences for display
const currentPlaybackRate = this.userPreferences.getPreference('playbackRate');
const currentQuality = this.userPreferences.getPreference('quality');
// Get current preferences for display
const currentPlaybackRate =
this.userPreferences.getPreference("playbackRate");
const currentQuality = this.userPreferences.getPreference("quality");
// Format playback rate for display
const playbackRateLabel = currentPlaybackRate === 1 ? 'Normal' : `${currentPlaybackRate}`;
const qualityLabel = currentQuality.charAt(0).toUpperCase() + currentQuality.slice(1);
// Format playback rate for display
const playbackRateLabel =
currentPlaybackRate === 1 ? "Normal" : `${currentPlaybackRate}`;
const qualities = this.getAvailableQualities();
const activeQuality =
qualities.find((q) => q.value === currentQuality) || qualities[0];
const qualityLabelHTML =
activeQuality?.displayLabel ||
activeQuality?.label ||
(currentQuality ? String(currentQuality) : "Auto");
// 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">${playbackRateLabel}</span>
</div>
<div class="settings-item" data-setting="quality">
<span>Quality</span>
<span class="current-quality">${qualityLabel}</span>
</div>
`;
// Settings menu content
this.settingsOverlay.innerHTML = `
<div class="settings-header">Settings</div>
<div class="settings-item" data-setting="playback-speed">
<span class="settings-left">
<span class="vjs-icon-placeholder settings-item-svg">
<svg height="24" viewBox="0 0 24 24" width="24"><path d="M10,8v8l6-4L10,8L10,8z M6.3,5L5.7,4.2C7.2,3,9,2.2,11,2l0.1,1C9.3,3.2,7.7,3.9,6.3,5z M5,6.3L4.2,5.7C3,7.2,2.2,9,2,11 l1,.1C3.2,9.3,3.9,7.7,5,6.3z M5,17.7c-1.1-1.4-1.8-3.1-2-4.8L2,13c0.2,2,1,3.8,2.2,5.4L5,17.7z M11.1,21c-1.8-0.2-3.4-0.9-4.8-2 l-0.6,.8C7.2,21,9,21.8,11,22L11.1,21z M22,12c0-5.2-3.9-9.4-9-10l-0.1,1c4.6,.5,8.1,4.3,8.1,9s-3.5,8.5-8.1,9l0.1,1 C18.2,21.5,22,17.2,22,12z" fill="white"></path></svg>
</span>
<span>Playback speed</span></span>
<span class="settings-right">
<span class="current-speed">${playbackRateLabel}</span>
<span class="vjs-icon-placeholder vjs-icon-navigate-next"></span>
</span>
</div>
<div class="settings-item" data-setting="quality">
<span class="settings-left">
<span class="vjs-icon-placeholder settings-item-svg">
<svg height="24" viewBox="0 0 24 24" width="24"><path d="M15,17h6v1h-6V17z M11,17H3v1h8v2h1v-2v-1v-2h-1V17z M14,8h1V6V5V3h-1v2H3v1h11V8z M18,5v1h3V5H18z M6,14h1v-2v-1V9H6v2H3v1 h3V14z M10,12h11v-1H10V12z" fill="white"></path></svg>
</span>
<span>Quality</span></span>
<span class="settings-right">
<span class="current-quality">${qualityLabelHTML}</span>
<span class="vjs-icon-placeholder vjs-icon-navigate-next"></span>
</span>
</div>
`;
// Create speed submenu
this.createSpeedSubmenu();
// Create speed submenu
this.createSpeedSubmenu();
// Add to control bar
controlBar.el().appendChild(this.settingsOverlay);
}
// Create quality submenu
this.createQualitySubmenu(qualities, activeQuality?.value);
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 },
];
// Add to control bar
controlBar.el().appendChild(this.settingsOverlay);
}
this.speedSubmenu = document.createElement('div');
this.speedSubmenu.className = 'speed-submenu';
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 },
];
// Get current playback rate for highlighting
const currentRate = this.userPreferences.getPreference('playbackRate');
this.speedSubmenu = document.createElement("div");
this.speedSubmenu.className = "speed-submenu";
this.speedSubmenu.innerHTML = `
// Get current playback rate for highlighting
const currentRate = this.userPreferences.getPreference("playbackRate");
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 === currentRate ? 'active' : ''}" data-speed="${option.value}">
.map(
(option) => `
<div class="speed-option ${option.value === currentRate ? "active" : ""}" data-speed="${option.value}">
<span>${option.label}</span>
${option.value === currentRate ? '<span>✓</span>' : ''}
${option.value === currentRate ? '<span class="checkmark">✓</span>' : ""}
</div>
`
)
.join('')}
)
.join("")}
`;
this.settingsOverlay.appendChild(this.speedSubmenu);
this.settingsOverlay.appendChild(this.speedSubmenu);
}
createQualitySubmenu(qualities, currentValue) {
this.qualitySubmenu = document.createElement("div");
this.qualitySubmenu.className = "quality-submenu";
const header = `
<div class="submenu-header">
<span style="margin-right: 8px;"></span>
<span>Quality</span>
</div>
`;
const optionsHtml = qualities
.map(
(q) => `
<div class="quality-option ${q.value === currentValue ? "active" : ""}" data-quality="${q.value}">
<span class="quality-label">${q.displayLabel || q.label}</span>
${q.value === currentValue ? '<span class="checkmark">✓</span>' : ""}
</div>
`
)
.join("");
this.qualitySubmenu.innerHTML = header + optionsHtml;
this.settingsOverlay.appendChild(this.qualitySubmenu);
}
getAvailableQualities() {
// Priority: provided options -> MEDIA_DATA JSON -> player sources -> default
const desiredOrder = [
"auto",
"144p",
"240p",
"360p",
"480p",
"720p",
"1080p",
];
if (
Array.isArray(this.providedQualities) &&
this.providedQualities.length
) {
return this.sortAndDecorateQualities(
this.providedQualities,
desiredOrder
);
}
positionButton() {
const controlBar = this.player().getChild('controlBar');
const fullscreenToggle = controlBar.getChild('fullscreenToggle');
try {
const md = typeof window !== "undefined" ? window.MEDIA_DATA : null;
const jsonQualities = md?.data?.qualities;
if (Array.isArray(jsonQualities) && jsonQualities.length) {
// Expected format: [{label: '1080p', value: '1080p', src: '...'}]
const normalized = jsonQualities.map((q) => ({
label: q.label || q.value || "Auto",
value: (q.value || q.label || "auto").toString().toLowerCase(),
src: q.src || q.url || q.href,
type: q.type || "video/mp4",
}));
return this.sortAndDecorateQualities(normalized, desiredOrder);
}
} catch (e) {
// ignore
}
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);
// Derive from player's current sources
const sources = this.player().currentSources
? this.player().currentSources()
: this.player().currentSrc();
if (Array.isArray(sources) && sources.length > 0) {
const mapped = sources.map((s, idx) => {
const label =
s.label ||
s.res ||
this.inferLabelFromSrc(s.src) ||
(idx === 0 ? "Auto" : `Source ${idx + 1}`);
const value = String(label).toLowerCase();
return { label, value, src: s.src, type: s.type || "video/mp4" };
});
return this.sortAndDecorateQualities(mapped, desiredOrder);
}
// Default fallback
// Build full ordered list without src so UI is consistent; switching will require src in JSON
const fallback = desiredOrder.map((v) => ({
label: v === "auto" ? "Auto" : v,
value: v,
}));
return this.sortAndDecorateQualities(fallback, desiredOrder);
}
sortAndDecorateQualities(list, desiredOrder) {
const orderIndex = (val) => {
const i = desiredOrder.indexOf(String(val).toLowerCase());
return i === -1 ? 999 : i;
};
const decorated = list
.map((q) => {
const val = (q.value || q.label || "").toString().toLowerCase();
const baseLabel = q.label || q.value || "";
const is1080 = val === "1080p";
const displayLabel = is1080
? `${baseLabel} <sup class="hd-badge">HD</sup>`
: baseLabel;
return { ...q, value: val, label: baseLabel, displayLabel };
})
.sort((a, b) => orderIndex(a.value) - orderIndex(b.value));
// Ensure all desired labels appear at least once (even if not provided), for consistent menu
const have = new Set(decorated.map((q) => q.value));
desiredOrder.forEach((val) => {
if (!have.has(val)) {
const baseLabel = val === "auto" ? "Auto" : val;
const displayLabel =
val === "1080p"
? `${baseLabel} <sup class="hd-badge">HD</sup>`
: baseLabel;
decorated.push({ label: baseLabel, value: val, displayLabel });
}
});
// Re-sort after pushing missing
decorated.sort((a, b) => orderIndex(a.value) - orderIndex(b.value));
return decorated;
}
inferLabelFromSrc(src) {
if (!src) return null;
// Try to detect typical resolution markers in file name or query string
const match = /(?:_|\.|\/)\D*(1440p|1080p|720p|480p|360p|240p|144p)/i.exec(
src
);
if (match && match[1]) return match[1].toUpperCase();
const m2 = /(\b\d{3,4})p\b/i.exec(src);
if (m2 && m2[1]) return `${m2[1]}p`;
return null;
}
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";
this.qualitySubmenu.style.display = "none";
}
if (e.target.closest('[data-setting="quality"]')) {
this.qualitySubmenu.style.display = "flex";
this.speedSubmenu.style.display = "none";
}
});
// Speed submenu header (back button)
this.speedSubmenu
.querySelector(".submenu-header")
.addEventListener("click", () => {
this.speedSubmenu.style.display = "none";
});
// Quality submenu header (back button)
this.qualitySubmenu
.querySelector(".submenu-header")
.addEventListener("click", () => {
this.qualitySubmenu.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);
}
});
// Quality option clicks
this.qualitySubmenu.addEventListener("click", (e) => {
const qualityOption = e.target.closest(".quality-option");
if (qualityOption) {
const value = qualityOption.dataset.quality;
this.handleQualityChange(value, qualityOption);
}
});
// 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
if (this.qualitySubmenu) this.qualitySubmenu.style.display = "none";
}
handleSpeedChange(speed, speedOption) {
// Update player speed
this.player().playbackRate(speed);
// Save preference
this.userPreferences.setPreference("playbackRate", speed);
// Update UI
this.speedSubmenu.querySelectorAll(".speed-option").forEach((opt) => {
opt.classList.remove("active");
opt.style.background = "transparent";
const check = opt.querySelector(".checkmark");
if (check) check.remove();
});
speedOption.classList.add("active");
speedOption.style.background = "rgba(255, 255, 255, 0.1)";
speedOption.insertAdjacentHTML(
"beforeend",
'<span class="checkmark">✓</span>'
);
// Update main menu display
const currentSpeedDisplay =
this.settingsOverlay.querySelector(".current-speed");
const speedLabel = speed === 1 ? "Normal" : `${speed}`;
currentSpeedDisplay.textContent = speedLabel;
// Close only the speed submenu (keep overlay open)
this.speedSubmenu.style.display = "none";
console.log("Playback speed preference saved:", speed);
}
handleQualityChange(value, qualityOption) {
const qualities = this.getAvailableQualities();
const selected = qualities.find((q) => String(q.value) === String(value));
// Save preference
this.userPreferences.setQualityPreference(value);
// Update UI
this.qualitySubmenu.querySelectorAll(".quality-option").forEach((opt) => {
opt.classList.remove("active");
opt.style.background = "transparent";
const check = opt.querySelector(".checkmark");
if (check) check.remove();
});
qualityOption.classList.add("active");
qualityOption.style.background = "rgba(255, 255, 255, 0.1)";
qualityOption.insertAdjacentHTML(
"beforeend",
'<span class="checkmark">✓</span>'
);
// Update main menu display
const currentQualityDisplay =
this.settingsOverlay.querySelector(".current-quality");
currentQualityDisplay.innerHTML =
selected?.displayLabel || selected?.label || String(value);
// Perform source switch if we have src defined
if (selected?.src) {
const player = this.player();
const wasPaused = player.paused();
const currentTime = player.currentTime();
const rate = player.playbackRate();
// Try to preserve active subtitle track
const textTracks = player.textTracks();
let activeSubtitleLang = null;
for (let i = 0; i < textTracks.length; i++) {
const track = textTracks[i];
if (track.kind === "subtitles" && track.mode === "showing") {
activeSubtitleLang = track.language;
break;
}
}
}
setupEventListeners() {
// Settings item clicks
this.settingsOverlay.addEventListener('click', (e) => {
e.stopPropagation();
console.log("Switching quality to", selected.label, selected.src);
if (e.target.closest('[data-setting="playback-speed"]')) {
this.speedSubmenu.style.display = 'flex';
}
});
player.addClass("vjs-changing-resolution");
player.src({ src: selected.src, type: selected.type || "video/mp4" });
// 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);
// Save preference
this.userPreferences.setPreference('playbackRate', speed);
// Update UI
document.querySelectorAll('.speed-option').forEach((opt) => {
opt.classList.remove('active');
opt.style.background = 'transparent';
opt.querySelector('span:last-child')?.remove();
});
speedOption.classList.add('active');
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');
const speedLabel = speed === 1 ? 'Normal' : `${speed}`;
currentSpeedDisplay.textContent = speedLabel;
// Hide menus
this.settingsOverlay.style.display = 'none';
this.speedSubmenu.style.display = 'none';
console.log('Playback speed preference saved:', speed);
}
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();
const onLoaded = () => {
// Restore time, rate, subtitles
try {
player.playbackRate(rate);
} catch (e) {}
try {
if (!isNaN(currentTime)) player.currentTime(currentTime);
} catch (e) {}
if (!wasPaused) {
player.play().catch(() => {});
}
super.dispose();
// Restore subtitles
if (activeSubtitleLang) {
const tt = player.textTracks();
for (let i = 0; i < tt.length; i++) {
const t = tt[i];
if (t.kind === "subtitles") {
t.mode =
t.language === activeSubtitleLang ? "showing" : "disabled";
}
}
}
// Ensure Subtitles (CC) button remains visible after source switch
try {
const controlBar = player.getChild("controlBar");
const names = [
"subtitlesButton",
"textTrackButton",
"subsCapsButton",
];
for (const n of names) {
const btn = controlBar && controlBar.getChild(n);
if (btn) {
if (typeof btn.show === "function") btn.show();
const el = btn.el && btn.el();
if (el) {
el.style.display = "";
el.style.visibility = "";
}
}
}
} catch (e) {
// noop
}
player.removeClass("vjs-changing-resolution");
player.off("loadedmetadata", onLoaded);
};
player.on("loadedmetadata", onLoaded);
}
// Close overlay to avoid covering the CC button
if (this.qualitySubmenu) this.qualitySubmenu.style.display = "none";
this.settingsOverlay.style.display = "none";
console.log("Quality preference saved:", value);
}
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";
if (this.qualitySubmenu) this.qualitySubmenu.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';
CustomSettingsMenu.prototype.controlText_ = "Settings Menu";
// Register the component with Video.js
videojs.registerComponent('CustomSettingsMenu', CustomSettingsMenu);
videojs.registerComponent("CustomSettingsMenu", CustomSettingsMenu);
export default CustomSettingsMenu;

View File

@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client';
import './VideoJS.css';
import VideoJS from './VideoJS.jsx';
// import ChapterList from './components/chapter/ChapterList.jsx';
// Mount the components when the DOM is ready
const mountComponents = () => {
@ -11,7 +12,12 @@ const mountComponents = () => {
const root = createRoot(rootContainer);
root.render(
<StrictMode>
<VideoJS />
<div className='video-wrapper'>
<div className='video-box'>
<VideoJS />
</div>
{/* <ChapterList /> */}
</div>
</StrictMode>
);
}