mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-10 17:38:54 -05:00
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:
parent
d88f4a27cc
commit
3accbd29ce
@ -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;}
|
||||
156
frontend-tools/video-js/src/components/chapter/ChapterList.jsx
Normal file
156
frontend-tools/video-js/src/components/chapter/ChapterList.jsx
Normal 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;
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
// 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="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>Quality</span>
|
||||
<span class="current-quality">${qualityLabel}</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;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user