mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-06 07:28:53 -05:00
refactor everything related to video.js
This commit is contained in:
parent
9627ef45f2
commit
afaab453e1
1
frontend-tools/video-js/.gitignore
vendored
1
frontend-tools/video-js/.gitignore
vendored
@ -23,3 +23,4 @@ dist-ssr
|
|||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
.env
|
.env
|
||||||
|
yt.readme.md
|
||||||
|
|||||||
15
frontend-tools/video-js/index-embed-old.html
Normal file
15
frontend-tools/video-js/index-embed-old.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>VideoJS</title>
|
||||||
|
</head>
|
||||||
|
<body style="padding: 0; margin: 0">
|
||||||
|
<div id="page-embed">
|
||||||
|
<div id="video-js-root-embed-old" class="video-js-root-embed-old"></div>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
13
frontend-tools/video-js/index-old.html
Normal file
13
frontend-tools/video-js/index-old.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>VideoJS</title>
|
||||||
|
</head>
|
||||||
|
<body style="padding: 0; margin: 0">
|
||||||
|
<div id="video-js-root-main-old" class="video-js-root-main-old"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -8,6 +8,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body style="padding: 0; margin: 0">
|
<body style="padding: 0; margin: 0">
|
||||||
<div id="video-js-root-main" class="video-js-root-main"></div>
|
<div id="video-js-root-main" class="video-js-root-main"></div>
|
||||||
|
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -515,14 +515,6 @@ html {
|
|||||||
background: rgba(255, 255, 255, 0.5) !important;
|
background: rgba(255, 255, 255, 0.5) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide HLS buffer segment boundaries while keeping overall load progress */
|
|
||||||
/* Target only the HLS buffer segment divs (they have data-start and data-end attributes) */
|
|
||||||
.vjs-load-progress > div[data-start][data-end] {
|
|
||||||
background: transparent !important;
|
|
||||||
border: none !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-js .vjs-progress-control {
|
.video-js .vjs-progress-control {
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
bottom: 42px !important; /* Moved up to prevent overlap with controls */
|
bottom: 42px !important; /* Moved up to prevent overlap with controls */
|
||||||
|
|||||||
8
frontend-tools/video-js/src/VideoJSNew.jsx
Normal file
8
frontend-tools/video-js/src/VideoJSNew.jsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { VideoJSPlayerNew } from './components';
|
||||||
|
|
||||||
|
function VideoJSNew({ videoId = 'default-video', ...props }) {
|
||||||
|
return <VideoJSPlayerNew videoId={videoId} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VideoJSNew;
|
||||||
@ -1,9 +1,19 @@
|
|||||||
/* ===== AUTOPLAY TOGGLE BUTTON STYLES ===== */
|
/* ===== AUTOPLAY TOGGLE BUTTON STYLES ===== */
|
||||||
|
|
||||||
.vjs-autoplay-toggle .vjs-autoplay-icon svg {
|
/* Font icon styles for autoplay button */
|
||||||
width: 100%;
|
.vjs-autoplay-toggle .vjs-icon-placeholder:before {
|
||||||
height: 100%;
|
font-size: 1.5em;
|
||||||
display: block;
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Use play icon when autoplay is OFF (clicking will turn it ON) */
|
||||||
|
.vjs-autoplay-toggle .vjs-icon-play:before {
|
||||||
|
content: "\f101"; /* VideoJS play icon */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Use pause icon when autoplay is ON (clicking will turn it OFF) */
|
||||||
|
.vjs-autoplay-toggle .vjs-icon-pause:before {
|
||||||
|
content: "\f103"; /* VideoJS pause icon */
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-js .vjs-autoplay-toggle {
|
.video-js .vjs-autoplay-toggle {
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import videojs from 'video.js';
|
import videojs from 'video.js';
|
||||||
import './AutoplayToggleButton.css';
|
// import './AutoplayToggleButton.css';
|
||||||
import autoPlayIconUrl from '../../assets/icons/autoplay-video-js-play.svg';
|
|
||||||
import autoPauseIconUrl from '../../assets/icons/autoplay-video-js-pause.svg';
|
|
||||||
|
|
||||||
const Button = videojs.getComponent('Button');
|
const Button = videojs.getComponent('Button');
|
||||||
|
|
||||||
@ -9,6 +7,20 @@ const Button = videojs.getComponent('Button');
|
|||||||
class AutoplayToggleButton extends Button {
|
class AutoplayToggleButton extends Button {
|
||||||
constructor(player, options) {
|
constructor(player, options) {
|
||||||
super(player, options);
|
super(player, options);
|
||||||
|
|
||||||
|
// Check if this is a touch device - don't create button on touch devices
|
||||||
|
const isTouchDevice =
|
||||||
|
options.isTouchDevice ||
|
||||||
|
'ontouchstart' in window ||
|
||||||
|
navigator.maxTouchPoints > 0 ||
|
||||||
|
navigator.msMaxTouchPoints > 0;
|
||||||
|
|
||||||
|
if (isTouchDevice) {
|
||||||
|
// Hide the button on touch devices
|
||||||
|
this.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.userPreferences = options.userPreferences;
|
this.userPreferences = options.userPreferences;
|
||||||
// Get autoplay preference from localStorage, default to false if not set
|
// Get autoplay preference from localStorage, default to false if not set
|
||||||
if (this.userPreferences) {
|
if (this.userPreferences) {
|
||||||
@ -31,18 +43,14 @@ class AutoplayToggleButton extends Button {
|
|||||||
'aria-label': this.isAutoplayEnabled ? 'Autoplay is on' : 'Autoplay is off',
|
'aria-label': this.isAutoplayEnabled ? 'Autoplay is on' : 'Autoplay is off',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create simple text-based icon for now to ensure it works
|
// Create icon placeholder using VideoJS icon system
|
||||||
this.iconSpan = videojs.dom.createEl('span', {
|
this.iconSpan = videojs.dom.createEl('span', {
|
||||||
'aria-hidden': 'true',
|
'aria-hidden': 'true',
|
||||||
className: 'vjs-autoplay-icon',
|
className: 'vjs-icon-placeholder vjs-autoplay-icon',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set initial icon state directly
|
// Set initial icon state using font icons
|
||||||
if (this.isAutoplayEnabled) {
|
this.updateIconClass();
|
||||||
this.iconSpan.innerHTML = `<img src="${autoPauseIconUrl}" alt="Autoplay on" style="width: 26px; height: 26px;" />`;
|
|
||||||
} else {
|
|
||||||
this.iconSpan.innerHTML = `<img src="${autoPlayIconUrl}" alt="Autoplay off" style="width: 26px; height: 26px;" />`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create control text span
|
// Create control text span
|
||||||
const controlTextSpan = videojs.dom.createEl('span', {
|
const controlTextSpan = videojs.dom.createEl('span', {
|
||||||
@ -60,29 +68,33 @@ class AutoplayToggleButton extends Button {
|
|||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateIconClass() {
|
||||||
|
// Remove existing icon classes
|
||||||
|
this.iconSpan.className = 'vjs-icon-placeholder vjs-autoplay-icon';
|
||||||
|
|
||||||
|
// Add appropriate icon class based on state
|
||||||
|
if (this.isAutoplayEnabled) {
|
||||||
|
this.iconSpan.classList.add('vjs-icon-spinner');
|
||||||
|
} else {
|
||||||
|
this.iconSpan.classList.add('vjs-icon-play-circle');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateIcon() {
|
updateIcon() {
|
||||||
// Add transition and start fade-out
|
// Add transition and start fade-out
|
||||||
this.iconSpan.style.transition = 'opacity 0.1s ease';
|
this.iconSpan.style.transition = 'opacity 0.1s ease';
|
||||||
this.iconSpan.style.opacity = '0';
|
this.iconSpan.style.opacity = '0';
|
||||||
|
|
||||||
// After fade-out complete, update innerHTML and fade back in
|
// After fade-out complete, update icon class and fade back in
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this.isAutoplayEnabled) {
|
this.updateIconClass();
|
||||||
this.iconSpan.innerHTML = `<img src="${autoPauseIconUrl}" alt="Autoplay on" style="width: 26px; height: 26px;" />`;
|
|
||||||
if (this.el()) {
|
if (this.el()) {
|
||||||
this.el().title = 'Autoplay is on';
|
this.el().title = this.isAutoplayEnabled ? 'Autoplay is on' : 'Autoplay is off';
|
||||||
this.el().setAttribute('aria-label', 'Autoplay is on');
|
this.el().setAttribute('aria-label', this.isAutoplayEnabled ? 'Autoplay is on' : 'Autoplay is off');
|
||||||
const controlText = this.el().querySelector('.vjs-control-text');
|
const controlText = this.el().querySelector('.vjs-control-text');
|
||||||
if (controlText) controlText.textContent = 'Autoplay is on';
|
if (controlText)
|
||||||
}
|
controlText.textContent = this.isAutoplayEnabled ? 'Autoplay is on' : 'Autoplay is off';
|
||||||
} else {
|
|
||||||
this.iconSpan.innerHTML = `<img src="${autoPlayIconUrl}" alt="Autoplay off" style="width: 26px; height: 26px;" />`;
|
|
||||||
if (this.el()) {
|
|
||||||
this.el().title = 'Autoplay is off';
|
|
||||||
this.el().setAttribute('aria-label', 'Autoplay is off');
|
|
||||||
const controlText = this.el().querySelector('.vjs-control-text');
|
|
||||||
if (controlText) controlText.textContent = 'Autoplay is off';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fade back in
|
// Fade back in
|
||||||
@ -127,14 +139,12 @@ class AutoplayToggleButton extends Button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let touchStartTime = 0;
|
let touchStartTime = 0;
|
||||||
let touchHandled = false;
|
|
||||||
|
|
||||||
// Touch start
|
// Touch start
|
||||||
button.addEventListener(
|
button.addEventListener(
|
||||||
'touchstart',
|
'touchstart',
|
||||||
(e) => {
|
() => {
|
||||||
touchStartTime = Date.now();
|
touchStartTime = Date.now();
|
||||||
touchHandled = false;
|
|
||||||
},
|
},
|
||||||
{ passive: true }
|
{ passive: true }
|
||||||
);
|
);
|
||||||
@ -153,7 +163,6 @@ class AutoplayToggleButton extends Button {
|
|||||||
|
|
||||||
// Show tooltip briefly
|
// Show tooltip briefly
|
||||||
button.classList.add('touch-active');
|
button.classList.add('touch-active');
|
||||||
touchHandled = true;
|
|
||||||
|
|
||||||
// Hide tooltip after shorter delay on mobile
|
// Hide tooltip after shorter delay on mobile
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { ReactComponent as PlayIcon } from '/autoplay-video-js-play.svg';
|
|
||||||
import { ReactComponent as PauseIcon } from '/autoplay-video-js-pause.svg';
|
|
||||||
|
|
||||||
const AutoplayToggleButton = ({ isAutoplayEnabled, onToggle, className = '' }) => {
|
|
||||||
const handleClick = () => {
|
|
||||||
onToggle(!isAutoplayEnabled);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={`vjs-autoplay-toggle vjs-control vjs-button ${className}`}
|
|
||||||
type="button"
|
|
||||||
title={isAutoplayEnabled ? 'Autoplay is on' : 'Autoplay is off'}
|
|
||||||
aria-label={isAutoplayEnabled ? 'Autoplay is on' : 'Autoplay is off'}
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
<span aria-hidden="true" className="vjs-autoplay-icon">
|
|
||||||
{isAutoplayEnabled ? (
|
|
||||||
<PlayIcon style={{ width: '26px', height: '26px' }} />
|
|
||||||
) : (
|
|
||||||
<PauseIcon style={{ width: '26px', height: '26px' }} />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span className="vjs-control-text">{isAutoplayEnabled ? 'Autoplay is on' : 'Autoplay is off'}</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AutoplayToggleButton;
|
|
||||||
@ -0,0 +1,210 @@
|
|||||||
|
/* ===== UNIFIED BUTTON TOOLTIP SYSTEM ===== */
|
||||||
|
/* Comprehensive tooltip styles for all VideoJS buttons */
|
||||||
|
|
||||||
|
/* Base tooltip styles using ::after pseudo-element */
|
||||||
|
.video-js .vjs-control-bar .vjs-control {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Universal tooltip styling for all buttons */
|
||||||
|
.video-js .vjs-control-bar .vjs-control::after {
|
||||||
|
content: attr(title);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
color: white;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition:
|
||||||
|
opacity 0.2s ease,
|
||||||
|
visibility 0.2s ease,
|
||||||
|
transform 0.2s ease;
|
||||||
|
z-index: 1000;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
|
||||||
|
"Droid Sans", "Helvetica Neue", sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show tooltip on hover and focus for desktop */
|
||||||
|
@media (hover: hover) and (pointer: fine) {
|
||||||
|
.video-js .vjs-control-bar .vjs-control:hover::after,
|
||||||
|
.video-js .vjs-control-bar .vjs-control:focus::after {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transform: translateX(-50%) translateY(-2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specific button tooltips - override content when needed */
|
||||||
|
.video-js .vjs-play-control::after {
|
||||||
|
content: attr(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js .vjs-mute-control::after {
|
||||||
|
content: attr(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js .vjs-fullscreen-control::after {
|
||||||
|
content: attr(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js .vjs-picture-in-picture-toggle::after {
|
||||||
|
content: attr(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js .vjs-subtitles-button::after {
|
||||||
|
content: attr(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js .vjs-chapters-button::after {
|
||||||
|
content: attr(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom button tooltips */
|
||||||
|
.video-js .vjs-autoplay-toggle::after {
|
||||||
|
content: attr(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js .vjs-next-video-button::after {
|
||||||
|
content: attr(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js .vjs-settings-button::after {
|
||||||
|
content: attr(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js .vjs-remaining-time::after {
|
||||||
|
content: attr(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Touch device support - show tooltips on tap */
|
||||||
|
@media (hover: none) and (pointer: coarse) {
|
||||||
|
/* Hide tooltips by default on touch devices */
|
||||||
|
.video-js .vjs-control-bar .vjs-control::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show tooltip when touch-active class is added */
|
||||||
|
.video-js .vjs-control-bar .vjs-control.touch-tooltip-active::after {
|
||||||
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transform: translateX(-50%) translateY(-2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet-specific adjustments */
|
||||||
|
@media (min-width: 768px) and (max-width: 1024px) and (hover: none) {
|
||||||
|
.video-js .vjs-control-bar .vjs-control.touch-tooltip-active::after {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile-specific adjustments */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.video-js .vjs-control-bar .vjs-control.touch-tooltip-active::after {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Exclude volume and time components from tooltips */
|
||||||
|
.video-js .vjs-volume-panel::after,
|
||||||
|
.video-js .vjs-volume-panel::before,
|
||||||
|
.video-js .vjs-mute-control::after,
|
||||||
|
.video-js .vjs-mute-control::before,
|
||||||
|
.video-js .vjs-volume-control::after,
|
||||||
|
.video-js .vjs-volume-control::before,
|
||||||
|
.video-js .vjs-volume-bar::after,
|
||||||
|
.video-js .vjs-volume-bar::before,
|
||||||
|
.video-js .vjs-remaining-time::after,
|
||||||
|
.video-js .vjs-current-time-display::after,
|
||||||
|
.video-js .vjs-duration-display::after,
|
||||||
|
.video-js .vjs-progress-control::after {
|
||||||
|
display: none !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
content: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specifically target volume panel and all its children to remove tooltips */
|
||||||
|
.video-js .vjs-volume-panel[title],
|
||||||
|
.video-js .vjs-volume-panel *[title],
|
||||||
|
.video-js .vjs-mute-control[title],
|
||||||
|
.video-js .vjs-volume-control[title],
|
||||||
|
.video-js .vjs-volume-control *[title],
|
||||||
|
.video-js .vjs-volume-bar[title] {
|
||||||
|
/* These selectors target elements with title attributes */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force remove tooltips from volume components using attribute selector */
|
||||||
|
.video-js .vjs-volume-panel,
|
||||||
|
.video-js .vjs-mute-control,
|
||||||
|
.video-js .vjs-volume-control {
|
||||||
|
/* Remove title attribute via CSS (not possible, but we can override the tooltip) */
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js .vjs-volume-panel:hover::after,
|
||||||
|
.video-js .vjs-volume-panel:focus::after,
|
||||||
|
.video-js .vjs-mute-control:hover::after,
|
||||||
|
.video-js .vjs-mute-control:focus::after {
|
||||||
|
display: none !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
content: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip arrow removed - no more triangles */
|
||||||
|
.video-js .vjs-control-bar .vjs-control::before {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disable native VideoJS tooltips to prevent conflicts */
|
||||||
|
.video-js .vjs-control-bar .vjs-control .vjs-control-text {
|
||||||
|
position: absolute !important;
|
||||||
|
left: -9999px !important;
|
||||||
|
width: 1px !important;
|
||||||
|
height: 1px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
clip: rect(1px, 1px, 1px, 1px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specifically hide play/pause button text that appears inside the icon */
|
||||||
|
.video-js .vjs-play-control .vjs-control-text,
|
||||||
|
.video-js .vjs-play-control span.vjs-control-text {
|
||||||
|
display: none !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override VideoJS native control text tooltips completely */
|
||||||
|
.video-js button.vjs-button:hover span.vjs-control-text {
|
||||||
|
opacity: 0 !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Re-enable for screen readers only when focused */
|
||||||
|
.video-js .vjs-control-bar .vjs-control:focus .vjs-control-text {
|
||||||
|
position: absolute !important;
|
||||||
|
left: -9999px !important;
|
||||||
|
width: 1px !important;
|
||||||
|
height: 1px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
clip: rect(1px, 1px, 1px, 1px) !important;
|
||||||
|
}
|
||||||
@ -1,55 +0,0 @@
|
|||||||
/* ===== CUSTOM REMAINING TIME STYLES ===== */
|
|
||||||
|
|
||||||
.vjs-control-bar .custom-remaining-time .vjs-remaining-time-display {
|
|
||||||
font-size: 14px !important;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
color: #fff;
|
|
||||||
white-space: nowrap;
|
|
||||||
padding: 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-control-bar .custom-remaining-time {
|
|
||||||
flex-shrink: 1 !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive time display sizing */
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.vjs-control-bar .custom-remaining-time .vjs-remaining-time-display {
|
|
||||||
font-size: 13px !important;
|
|
||||||
padding: 0 3px;
|
|
||||||
font-weight: 600 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
.vjs-control-bar .custom-remaining-time .vjs-remaining-time-display {
|
|
||||||
font-size: 12px !important;
|
|
||||||
padding: 0 2px;
|
|
||||||
letter-spacing: -0.2px;
|
|
||||||
font-weight: 600 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.vjs-control-bar .custom-remaining-time .vjs-remaining-time-display {
|
|
||||||
font-size: 11px !important;
|
|
||||||
padding: 0 2px;
|
|
||||||
letter-spacing: -0.2px;
|
|
||||||
font-weight: 600 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 399px) {
|
|
||||||
.vjs-control-bar .custom-remaining-time .vjs-remaining-time-display {
|
|
||||||
font-size: 10px !important;
|
|
||||||
padding: 0 1px;
|
|
||||||
letter-spacing: -0.3px;
|
|
||||||
font-weight: 600 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +1,4 @@
|
|||||||
// components/controls/CustomRemainingTime.js
|
|
||||||
import videojs from 'video.js';
|
import videojs from 'video.js';
|
||||||
import './CustomRemainingTime.css';
|
|
||||||
|
|
||||||
// Get the Component base class from Video.js
|
// Get the Component base class from Video.js
|
||||||
const Component = videojs.getComponent('Component');
|
const Component = videojs.getComponent('Component');
|
||||||
@ -31,18 +29,71 @@ class CustomRemainingTime extends Component {
|
|||||||
*/
|
*/
|
||||||
createEl() {
|
createEl() {
|
||||||
const el = videojs.dom.createEl('div', {
|
const el = videojs.dom.createEl('div', {
|
||||||
className: 'vjs-remaining-time vjs-time-control vjs-control custom-remaining-time',
|
className: 'vjs-remaining-time vjs-time-control vjs-control',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add ARIA accessibility
|
// Add ARIA accessibility
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<span class="vjs-control-text" role="presentation">Time Display </span>
|
<span class="vjs-remaining-time-display" role="timer" aria-live="off">0:00 / 0:00</span>
|
||||||
<span class="vjs-remaining-time-display" role="presentation">0:00 / 0:00</span>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add touch tooltip support for mobile devices
|
||||||
|
*/
|
||||||
|
addTouchTooltipSupport(element) {
|
||||||
|
// Check if device is touch-enabled
|
||||||
|
const isTouchDevice =
|
||||||
|
'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
||||||
|
|
||||||
|
// Only add touch tooltip support on actual touch devices
|
||||||
|
if (!isTouchDevice) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let touchStartTime = 0;
|
||||||
|
let tooltipTimeout = null;
|
||||||
|
|
||||||
|
// Touch start
|
||||||
|
element.addEventListener(
|
||||||
|
'touchstart',
|
||||||
|
() => {
|
||||||
|
touchStartTime = Date.now();
|
||||||
|
},
|
||||||
|
{ passive: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Touch end
|
||||||
|
element.addEventListener(
|
||||||
|
'touchend',
|
||||||
|
(e) => {
|
||||||
|
const touchDuration = Date.now() - touchStartTime;
|
||||||
|
|
||||||
|
// Only show tooltip for quick taps (not swipes)
|
||||||
|
if (touchDuration < 300) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Show tooltip briefly
|
||||||
|
element.classList.add('touch-tooltip-active');
|
||||||
|
|
||||||
|
// Clear any existing timeout
|
||||||
|
if (tooltipTimeout) {
|
||||||
|
clearTimeout(tooltipTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide tooltip after delay
|
||||||
|
tooltipTimeout = setTimeout(() => {
|
||||||
|
element.classList.remove('touch-tooltip-active');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ passive: false }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the time display
|
* Update the time display
|
||||||
*/
|
*/
|
||||||
@ -90,7 +141,7 @@ class CustomRemainingTime extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set component name for Video.js
|
// Set component name for Video.js
|
||||||
CustomRemainingTime.prototype.controlText_ = 'Time Display';
|
CustomRemainingTime.prototype.controlText_ = '';
|
||||||
|
|
||||||
// Register the component with Video.js
|
// Register the component with Video.js
|
||||||
videojs.registerComponent('CustomRemainingTime', CustomRemainingTime);
|
videojs.registerComponent('CustomRemainingTime', CustomRemainingTime);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// components/controls/CustomSettingsMenu.js
|
// components/controls/CustomSettingsMenu.js
|
||||||
import videojs from 'video.js';
|
import videojs from 'video.js';
|
||||||
import './CustomSettingsMenu.css';
|
import './CustomSettingsMenu.css';
|
||||||
import './SettingsButton.css';
|
// import './SettingsButton.css';
|
||||||
import UserPreferences from '../../utils/UserPreferences';
|
import UserPreferences from '../../utils/UserPreferences';
|
||||||
|
|
||||||
// Get the Component base class from Video.js
|
// Get the Component base class from Video.js
|
||||||
@ -53,19 +53,26 @@ class CustomSettingsMenu extends Component {
|
|||||||
// Create settings button
|
// Create settings button
|
||||||
this.settingsButton = controlBar.addChild('button', {
|
this.settingsButton = controlBar.addChild('button', {
|
||||||
controlText: 'Settings',
|
controlText: 'Settings',
|
||||||
className: 'vjs-settings-button settings-clicked',
|
className: 'vjs-settings-button vjs-control vjs-button settings-clicked',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Style the settings button (gear icon)
|
// Style the settings button (gear icon)
|
||||||
const settingsButtonEl = this.settingsButton.el();
|
const settingsButtonEl = this.settingsButton.el();
|
||||||
settingsButtonEl.innerHTML = `
|
settingsButtonEl.innerHTML = `
|
||||||
<span><svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%"><use class="ytp-svg-shadow" xlink:href="#ytp-id-19"></use><path d="m 23.94,18.78 c .03,-0.25 .05,-0.51 .05,-0.78 0,-0.27 -0.02,-0.52 -0.05,-0.78 l 1.68,-1.32 c .15,-0.12 .19,-0.33 .09,-0.51 l -1.6,-2.76 c -0.09,-0.17 -0.31,-0.24 -0.48,-0.17 l -1.99,.8 c -0.41,-0.32 -0.86,-0.58 -1.35,-0.78 l -0.30,-2.12 c -0.02,-0.19 -0.19,-0.33 -0.39,-0.33 l -3.2,0 c -0.2,0 -0.36,.14 -0.39,.33 l -0.30,2.12 c -0.48,.2 -0.93,.47 -1.35,.78 l -1.99,-0.8 c -0.18,-0.07 -0.39,0 -0.48,.17 l -1.6,2.76 c -0.10,.17 -0.05,.39 .09,.51 l 1.68,1.32 c -0.03,.25 -0.05,.52 -0.05,.78 0,.26 .02,.52 .05,.78 l -1.68,1.32 c -0.15,.12 -0.19,.33 -0.09,.51 l 1.6,2.76 c .09,.17 .31,.24 .48,.17 l 1.99,-0.8 c .41,.32 .86,.58 1.35,.78 l .30,2.12 c .02,.19 .19,.33 .39,.33 l 3.2,0 c .2,0 .36,-0.14 .39,-0.33 l .30,-2.12 c .48,-0.2 .93,-0.47 1.35,-0.78 l 1.99,.8 c .18,.07 .39,0 .48,-0.17 l 1.6,-2.76 c .09,-0.17 .05,-0.39 -0.09,-0.51 l -1.68,-1.32 0,0 z m -5.94,2.01 c -1.54,0 -2.8,-1.25 -2.8,-2.8 0,-1.54 1.25,-2.8 2.8,-2.8 1.54,0 2.8,1.25 2.8,2.8 0,1.54 -1.25,2.8 -2.8,2.8 l 0,0 z" fill="#fff" id="ytp-id-19"></path></svg></span>
|
<span class="vjs-icon-placeholder vjs-icon-cog"></span>
|
||||||
<span class="vjs-control-text">Settings</span>
|
<span class="vjs-control-text">Settings</span>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Add tooltip attributes
|
||||||
|
settingsButtonEl.setAttribute('title', 'Settings');
|
||||||
|
settingsButtonEl.setAttribute('aria-label', 'Settings');
|
||||||
|
|
||||||
// Position the settings button at the end of the control bar
|
// Position the settings button at the end of the control bar
|
||||||
this.positionButton();
|
this.positionButton();
|
||||||
|
|
||||||
|
// Add touch tooltip support
|
||||||
|
this.addTouchTooltipSupport(settingsButtonEl);
|
||||||
|
|
||||||
// Add mobile touch handling and unified click handling
|
// Add mobile touch handling and unified click handling
|
||||||
const buttonEl = this.settingsButton.el();
|
const buttonEl = this.settingsButton.el();
|
||||||
if (buttonEl) {
|
if (buttonEl) {
|
||||||
@ -1154,6 +1161,57 @@ class CustomSettingsMenu extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add touch tooltip support for mobile devices
|
||||||
|
addTouchTooltipSupport(button) {
|
||||||
|
// Check if device is touch-enabled
|
||||||
|
const isTouchDevice =
|
||||||
|
'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
||||||
|
|
||||||
|
// Only add touch tooltip support on actual touch devices
|
||||||
|
if (!isTouchDevice) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let touchStartTime = 0;
|
||||||
|
let tooltipTimeout = null;
|
||||||
|
|
||||||
|
// Touch start
|
||||||
|
button.addEventListener(
|
||||||
|
'touchstart',
|
||||||
|
() => {
|
||||||
|
touchStartTime = Date.now();
|
||||||
|
},
|
||||||
|
{ passive: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Touch end
|
||||||
|
button.addEventListener(
|
||||||
|
'touchend',
|
||||||
|
(e) => {
|
||||||
|
const touchDuration = Date.now() - touchStartTime;
|
||||||
|
|
||||||
|
// Only show tooltip for quick taps (not swipes)
|
||||||
|
if (touchDuration < 300) {
|
||||||
|
// Don't prevent default here as it might interfere with the settings menu
|
||||||
|
|
||||||
|
// Show tooltip briefly
|
||||||
|
button.classList.add('touch-tooltip-active');
|
||||||
|
|
||||||
|
// Clear any existing timeout
|
||||||
|
if (tooltipTimeout) {
|
||||||
|
clearTimeout(tooltipTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide tooltip after delay
|
||||||
|
tooltipTimeout = setTimeout(() => {
|
||||||
|
button.classList.remove('touch-tooltip-active');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ passive: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
// Remove event listeners
|
// Remove event listeners
|
||||||
document.removeEventListener('click', this.handleClickOutside);
|
document.removeEventListener('click', this.handleClickOutside);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import videojs from 'video.js';
|
import videojs from 'video.js';
|
||||||
import './NextVideoButton.css';
|
// import './NextVideoButton.css';
|
||||||
|
|
||||||
const Button = videojs.getComponent('Button');
|
const Button = videojs.getComponent('Button');
|
||||||
|
|
||||||
@ -11,39 +11,98 @@ class NextVideoButton extends Button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createEl() {
|
createEl() {
|
||||||
const button = super.createEl('button', {
|
// Create button element directly without wrapper div
|
||||||
className: 'vjs-next-video-control vjs-control vjs-button',
|
const button = videojs.dom.createEl('button', {
|
||||||
|
className: 'vjs-next-video-button vjs-control vjs-button',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
title: 'Next Video',
|
title: 'Next Video',
|
||||||
'aria-label': 'Next Video',
|
'aria-label': 'Next Video',
|
||||||
|
'aria-disabled': 'false',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create the icon span using Video.js core icon
|
// Create the icon placeholder span (Video.js standard structure)
|
||||||
const iconSpan = videojs.dom.createEl('span', {
|
const iconPlaceholder = videojs.dom.createEl('span', {
|
||||||
|
className: 'vjs-icon-placeholder',
|
||||||
'aria-hidden': 'true',
|
'aria-hidden': 'true',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create SVG that matches Video.js icon dimensions
|
// Create control text span (Video.js standard structure)
|
||||||
iconSpan.innerHTML = `
|
|
||||||
<svg width="34" height="34" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M14 34L28.1667 24L14 14V34ZM30.6667 14V34H34V14H30.6667Z" fill="currentColor"/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Create control text span
|
|
||||||
const controlTextSpan = videojs.dom.createEl('span', {
|
const controlTextSpan = videojs.dom.createEl('span', {
|
||||||
className: 'vjs-control-text',
|
className: 'vjs-control-text',
|
||||||
|
'aria-live': 'polite',
|
||||||
});
|
});
|
||||||
controlTextSpan.textContent = 'Next Video';
|
controlTextSpan.textContent = 'Next Video';
|
||||||
|
|
||||||
// Append both spans to button
|
// Create custom icon span with SVG
|
||||||
button.appendChild(iconSpan);
|
const customIconSpan = videojs.dom.createEl('span');
|
||||||
|
customIconSpan.innerHTML = `
|
||||||
|
<svg width="11" height="11" viewBox="14 14 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14 34L28.1667 24L14 14V34ZM30.6667 14V34H34V14H30.6667Z" fill="currentColor"></path>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
// Append spans to button in Video.js standard order
|
||||||
|
button.appendChild(iconPlaceholder);
|
||||||
button.appendChild(controlTextSpan);
|
button.appendChild(controlTextSpan);
|
||||||
|
button.appendChild(customIconSpan);
|
||||||
|
|
||||||
|
// Add touch tooltip support
|
||||||
|
this.addTouchTooltipSupport(button);
|
||||||
|
|
||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add touch tooltip support for mobile devices
|
||||||
|
addTouchTooltipSupport(button) {
|
||||||
|
// Check if device is touch-enabled
|
||||||
|
const isTouchDevice =
|
||||||
|
'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
||||||
|
|
||||||
|
// Only add touch tooltip support on actual touch devices
|
||||||
|
if (!isTouchDevice) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let touchStartTime = 0;
|
||||||
|
let tooltipTimeout = null;
|
||||||
|
|
||||||
|
// Touch start
|
||||||
|
button.addEventListener(
|
||||||
|
'touchstart',
|
||||||
|
() => {
|
||||||
|
touchStartTime = Date.now();
|
||||||
|
},
|
||||||
|
{ passive: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Touch end
|
||||||
|
button.addEventListener(
|
||||||
|
'touchend',
|
||||||
|
(e) => {
|
||||||
|
const touchDuration = Date.now() - touchStartTime;
|
||||||
|
|
||||||
|
// Only show tooltip for quick taps (not swipes)
|
||||||
|
if (touchDuration < 300) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Show tooltip briefly
|
||||||
|
button.classList.add('touch-tooltip-active');
|
||||||
|
|
||||||
|
// Clear any existing timeout
|
||||||
|
if (tooltipTimeout) {
|
||||||
|
clearTimeout(tooltipTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide tooltip after delay
|
||||||
|
tooltipTimeout = setTimeout(() => {
|
||||||
|
button.classList.remove('touch-tooltip-active');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ passive: false }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
handleClick() {
|
handleClick() {
|
||||||
this.player().trigger('nextVideo');
|
this.player().trigger('nextVideo');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import videojs from 'video.js';
|
import videojs from 'video.js';
|
||||||
import './SeekIndicator.css';
|
// import './SeekIndicator.css';
|
||||||
|
|
||||||
const Component = videojs.getComponent('Component');
|
const Component = videojs.getComponent('Component');
|
||||||
|
|
||||||
@ -10,6 +10,17 @@ class SeekIndicator extends Component {
|
|||||||
this.seekAmount = options.seekAmount || 5; // Default seek amount in seconds
|
this.seekAmount = options.seekAmount || 5; // Default seek amount in seconds
|
||||||
this.isEmbedPlayer = options.isEmbedPlayer || false; // Store embed mode flag
|
this.isEmbedPlayer = options.isEmbedPlayer || false; // Store embed mode flag
|
||||||
this.showTimeout = null;
|
this.showTimeout = null;
|
||||||
|
|
||||||
|
// Detect touch devices - if touch is supported, native browser controls will handle icons
|
||||||
|
this.isTouchDevice = this.detectTouchDevice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect if the device supports touch
|
||||||
|
* @returns {boolean} True if touch is supported
|
||||||
|
*/
|
||||||
|
detectTouchDevice() {
|
||||||
|
return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
createEl() {
|
createEl() {
|
||||||
@ -39,6 +50,11 @@ class SeekIndicator extends Component {
|
|||||||
* @param {number} seconds - Number of seconds to seek (only used for forward/backward)
|
* @param {number} seconds - Number of seconds to seek (only used for forward/backward)
|
||||||
*/
|
*/
|
||||||
show(direction, seconds = this.seekAmount) {
|
show(direction, seconds = this.seekAmount) {
|
||||||
|
// Skip showing icons on touch devices as native browser controls handle them
|
||||||
|
if (this.isTouchDevice) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const el = this.el();
|
const el = this.el();
|
||||||
const iconEl = el.querySelector('.vjs-seek-indicator-icon');
|
const iconEl = el.querySelector('.vjs-seek-indicator-icon');
|
||||||
const textEl = el.querySelector('.vjs-seek-indicator-text');
|
const textEl = el.querySelector('.vjs-seek-indicator-text');
|
||||||
@ -230,6 +246,11 @@ class SeekIndicator extends Component {
|
|||||||
* Show pause icon for mobile (uses 500ms from main show method)
|
* Show pause icon for mobile (uses 500ms from main show method)
|
||||||
*/
|
*/
|
||||||
showMobilePauseIcon() {
|
showMobilePauseIcon() {
|
||||||
|
// Skip showing icons on touch devices as native browser controls handle them
|
||||||
|
if (this.isTouchDevice) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.show('pause-mobile'); // This will auto-hide after 500ms
|
this.show('pause-mobile'); // This will auto-hide after 500ms
|
||||||
|
|
||||||
// Make the icon clickable for mobile
|
// Make the icon clickable for mobile
|
||||||
|
|||||||
@ -0,0 +1,97 @@
|
|||||||
|
import videojs from 'video.js';
|
||||||
|
|
||||||
|
const TransientButton = videojs.getComponent('TransientButton');
|
||||||
|
|
||||||
|
class TestButton extends TransientButton {
|
||||||
|
constructor(player, options) {
|
||||||
|
super(player, {
|
||||||
|
controlText: 'Test Button',
|
||||||
|
position: ['bottom', 'right'],
|
||||||
|
className: 'test-button',
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
this.setupVisibilityHandling();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupVisibilityHandling() {
|
||||||
|
// Add CSS transition for smooth fade out like control bar
|
||||||
|
this.el().style.transition = 'opacity 0.3s ease';
|
||||||
|
|
||||||
|
this.player().on('mouseenter', () => {
|
||||||
|
this.showWithFade();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.player().on('mouseleave', () => {
|
||||||
|
// Only hide if video is playing
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.player().paused()) {
|
||||||
|
this.hideWithFade();
|
||||||
|
}
|
||||||
|
}, 3000); // Hide after 3 seconds delay like control bar
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add touch events
|
||||||
|
this.player().on('touchstart', () => {
|
||||||
|
this.showWithFade();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.player().on('touchend', () => {
|
||||||
|
// Hide after a delay to allow for interaction, but only if playing
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.player().paused()) {
|
||||||
|
this.hideWithFade();
|
||||||
|
}
|
||||||
|
}, 3000); // Hide after 3 seconds delay
|
||||||
|
});
|
||||||
|
|
||||||
|
// Alternative: Use user activity events (recommended)
|
||||||
|
this.player().on('useractive', () => {
|
||||||
|
this.showWithFade();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.player().on('userinactive', () => {
|
||||||
|
// Only hide if video is playing
|
||||||
|
if (!this.player().paused()) {
|
||||||
|
this.hideWithFade();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show when paused, hide when playing
|
||||||
|
this.player().on('pause', () => {
|
||||||
|
this.showWithFade();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.player().on('play', () => {
|
||||||
|
// Hide when playing starts, unless user is actively interacting
|
||||||
|
if (!this.player().userActive()) {
|
||||||
|
this.hideWithFade();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showWithFade() {
|
||||||
|
this.show();
|
||||||
|
this.el().style.opacity = '1';
|
||||||
|
this.el().style.visibility = 'visible';
|
||||||
|
}
|
||||||
|
|
||||||
|
hideWithFade() {
|
||||||
|
// Start fade out transition
|
||||||
|
this.el().style.opacity = '0';
|
||||||
|
|
||||||
|
// Hide element after transition completes (300ms like control bar)
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.el().style.opacity === '0') {
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick() {
|
||||||
|
alert('testButton - controls were hidden');
|
||||||
|
// Add your custom functionality here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
videojs.registerComponent('TestButton', TestButton);
|
||||||
|
export default TestButton;
|
||||||
@ -1,5 +1,6 @@
|
|||||||
// Export all Video.js components
|
// Export all Video.js components
|
||||||
export { default as VideoJSPlayer } from './video-player/VideoJSPlayer';
|
export { default as VideoJSPlayer } from './video-player/VideoJSPlayer';
|
||||||
|
export { default as VideoJSPlayerNew } from './video-player/VideoJSPlayerNew';
|
||||||
export { default as EndScreenOverlay } from './overlays/EndScreenOverlay';
|
export { default as EndScreenOverlay } from './overlays/EndScreenOverlay';
|
||||||
export { default as AutoplayCountdownOverlay } from './overlays/AutoplayCountdownOverlay';
|
export { default as AutoplayCountdownOverlay } from './overlays/AutoplayCountdownOverlay';
|
||||||
export { default as ChapterMarkers } from './markers/ChapterMarkers';
|
export { default as ChapterMarkers } from './markers/ChapterMarkers';
|
||||||
|
|||||||
@ -63,21 +63,28 @@
|
|||||||
width: 166px !important;
|
width: 166px !important;
|
||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
height: 96px;
|
height: 96px;
|
||||||
margin: 0 auto 10px;
|
margin: 10px auto 10px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 3px solid #fff;
|
border: 3px solid #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vjs-chapter-floating-tooltip .chapter-title {
|
.vjs-chapter-floating-tooltip .chapter-title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0 0 10px;
|
margin: 0 0 5px;
|
||||||
font-weight: 700;
|
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vjs-chapter-floating-tooltip .position-info,
|
|
||||||
.vjs-chapter-floating-tooltip .chapter-info {
|
.vjs-chapter-floating-tooltip .chapter-info {
|
||||||
|
font-size: 15px;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 0 5px;
|
||||||
|
line-height: normal;
|
||||||
|
vertical-align: top;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-chapter-floating-tooltip .position-info {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 0 2px;
|
margin: 0 0 2px;
|
||||||
@ -96,63 +103,3 @@
|
|||||||
transform: translateX(-50%) translateY(0);
|
transform: translateX(-50%) translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Disable chapter marker tooltips on touch devices */
|
|
||||||
@media (hover: none) and (pointer: coarse) {
|
|
||||||
.vjs-chapter-marker:hover .vjs-chapter-marker-tooltip {
|
|
||||||
display: none !important;
|
|
||||||
opacity: 0 !important;
|
|
||||||
visibility: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-chapter-floating-tooltip {
|
|
||||||
display: none !important;
|
|
||||||
opacity: 0 !important;
|
|
||||||
visibility: hidden !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) and (max-width: 899px) {
|
|
||||||
/* Disable chapter marker tooltips on tablets */
|
|
||||||
.vjs-chapter-marker:hover .vjs-chapter-marker-tooltip {
|
|
||||||
opacity: 0 !important;
|
|
||||||
visibility: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Disable chapter floating tooltips on tablets */
|
|
||||||
.vjs-chapter-floating-tooltip {
|
|
||||||
display: none !important;
|
|
||||||
opacity: 0 !important;
|
|
||||||
visibility: hidden !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
/* Disable chapter marker tooltips on mobile */
|
|
||||||
.vjs-chapter-marker:hover .vjs-chapter-marker-tooltip {
|
|
||||||
opacity: 0 !important;
|
|
||||||
visibility: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Disable chapter floating tooltips on mobile */
|
|
||||||
.vjs-chapter-floating-tooltip {
|
|
||||||
display: none !important;
|
|
||||||
opacity: 0 !important;
|
|
||||||
visibility: hidden !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
/* Disable chapter marker tooltips on small mobile */
|
|
||||||
.vjs-chapter-marker:hover .vjs-chapter-marker-tooltip {
|
|
||||||
opacity: 0 !important;
|
|
||||||
visibility: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Disable chapter floating tooltips on small mobile */
|
|
||||||
.vjs-chapter-floating-tooltip {
|
|
||||||
display: none !important;
|
|
||||||
opacity: 0 !important;
|
|
||||||
visibility: hidden !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -105,7 +105,7 @@ class ChapterMarkers extends Component {
|
|||||||
Object.assign(this.tooltip.style, {
|
Object.assign(this.tooltip.style, {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
zIndex: '1000',
|
zIndex: '1000',
|
||||||
bottom: '45px',
|
bottom: '25px',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
display: 'none',
|
display: 'none',
|
||||||
minWidth: '160px',
|
minWidth: '160px',
|
||||||
@ -148,10 +148,10 @@ class ChapterMarkers extends Component {
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Append all elements to tooltip
|
// Append all elements to tooltip - duration after title, then image
|
||||||
this.tooltip.appendChild(this.chapterTitle);
|
this.tooltip.appendChild(this.chapterTitle);
|
||||||
this.tooltip.appendChild(this.chapterImage);
|
|
||||||
this.tooltip.appendChild(this.chapterInfo);
|
this.tooltip.appendChild(this.chapterInfo);
|
||||||
|
this.tooltip.appendChild(this.chapterImage);
|
||||||
this.tooltip.appendChild(this.positionInfo);
|
this.tooltip.appendChild(this.positionInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,39 +26,3 @@
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 3px solid #fff;
|
border: 3px solid #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Disable sprite preview tooltips on touch devices */
|
|
||||||
@media (hover: none) and (pointer: coarse) {
|
|
||||||
.vjs-sprite-preview-tooltip {
|
|
||||||
display: none !important;
|
|
||||||
opacity: 0 !important;
|
|
||||||
visibility: hidden !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) and (max-width: 899px) {
|
|
||||||
/* Disable sprite preview tooltips on tablets */
|
|
||||||
.vjs-sprite-preview-tooltip {
|
|
||||||
display: none !important;
|
|
||||||
opacity: 0 !important;
|
|
||||||
visibility: hidden !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
/* Disable sprite preview tooltips on mobile */
|
|
||||||
.vjs-sprite-preview-tooltip {
|
|
||||||
display: none !important;
|
|
||||||
opacity: 0 !important;
|
|
||||||
visibility: hidden !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
/* Disable sprite preview tooltips on small mobile */
|
|
||||||
.vjs-sprite-preview-tooltip {
|
|
||||||
display: none !important;
|
|
||||||
opacity: 0 !important;
|
|
||||||
visibility: hidden !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -63,7 +63,7 @@ class SpritePreview extends Component {
|
|||||||
bottom: '45px',
|
bottom: '45px',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
display: 'none',
|
display: 'none',
|
||||||
minWidth: '172px', // Accommodate 166px image + 3px border on each side
|
minWidth: '172px',
|
||||||
maxWidth: '172px',
|
maxWidth: '172px',
|
||||||
width: '172px',
|
width: '172px',
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,689 +1,3 @@
|
|||||||
/* ===== END SCREEN OVERLAY STYLES ===== */
|
.vjs-ended .vjs-poster {
|
||||||
|
|
||||||
.vjs-end-screen-overlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100% - 80px); /* Reduce reserved space for seekbar */
|
|
||||||
background: #000000;
|
|
||||||
display: none;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center; /* Center the grid vertically */
|
|
||||||
align-items: center;
|
|
||||||
padding: 40px 40px 40px 40px; /* Equal visual margins on all sides */
|
|
||||||
box-sizing: border-box;
|
|
||||||
z-index: 9999;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide poster image when video ends and end screen is shown */
|
|
||||||
.video-js.vjs-ended .vjs-poster {
|
|
||||||
display: none !important;
|
|
||||||
opacity: 0 !important;
|
|
||||||
visibility: hidden !important;
|
|
||||||
z-index: -1 !important;
|
|
||||||
width: 0 !important;
|
|
||||||
height: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide video element completely when ended */
|
|
||||||
.video-js.vjs-ended video {
|
|
||||||
display: none !important;
|
|
||||||
opacity: 0 !important;
|
|
||||||
visibility: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure the overlay covers everything with maximum z-index */
|
|
||||||
.video-js.vjs-ended .vjs-end-screen-overlay {
|
|
||||||
z-index: 99999 !important;
|
|
||||||
display: flex !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Embed-specific full page overlay */
|
|
||||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
height: calc(100vh - 80px) !important; /* Reduce reserved space for controls */
|
|
||||||
z-index: 9998 !important; /* Below controls but above video */
|
|
||||||
display: flex !important;
|
|
||||||
padding: 120px 40px 40px 40px !important; /* Top padding for embed info + equal visual margins */
|
|
||||||
justify-content: center !important; /* Center the grid vertically */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Small player size optimization - 2 items horizontally for better title readability */
|
|
||||||
/* This applies to both embed and regular players when they're small */
|
|
||||||
.vjs-end-screen-overlay.vjs-small-player .vjs-related-videos-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr) !important;
|
|
||||||
grid-template-rows: 1fr !important;
|
|
||||||
gap: 20px !important;
|
|
||||||
max-width: 600px; /* Limit width for better proportions */
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-end-screen-overlay.vjs-small-player {
|
|
||||||
height: calc(100% - 60px) !important;
|
|
||||||
padding: 30px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide items beyond the first 2 for small players */
|
|
||||||
.vjs-end-screen-overlay.vjs-small-player .vjs-related-video-item:nth-child(n + 3) {
|
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Embed-specific adjustments for small sizes */
|
|
||||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay.vjs-small-player {
|
|
||||||
height: calc(100vh - 60px) !important;
|
|
||||||
padding: 80px 30px 30px 30px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fallback media query for cases where class detection might not work */
|
|
||||||
@media (max-height: 500px), (max-width: 600px) {
|
|
||||||
.vjs-related-videos-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr) !important;
|
|
||||||
grid-template-rows: 1fr !important;
|
|
||||||
gap: 20px !important;
|
|
||||||
max-width: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-end-screen-overlay {
|
|
||||||
height: calc(100% - 60px) !important;
|
|
||||||
padding: 30px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-related-video-item:nth-child(n + 3) {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
|
||||||
height: calc(100vh - 60px) !important;
|
|
||||||
padding: 80px 30px 30px 30px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Very small player size - further optimize spacing (class-based detection) */
|
|
||||||
.vjs-end-screen-overlay.vjs-very-small-player .vjs-related-videos-grid {
|
|
||||||
gap: 15px !important;
|
|
||||||
max-width: 500px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-end-screen-overlay.vjs-very-small-player {
|
|
||||||
height: calc(100% - 50px) !important;
|
|
||||||
padding: 25px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-end-screen-overlay.vjs-very-small-player .vjs-related-video-item {
|
|
||||||
min-height: 80px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay.vjs-very-small-player {
|
|
||||||
height: calc(100vh - 50px) !important;
|
|
||||||
padding: 60px 25px 25px 25px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fallback media query for very small sizes */
|
|
||||||
@media (max-height: 400px), (max-width: 400px) {
|
|
||||||
.vjs-related-videos-grid {
|
|
||||||
gap: 15px !important;
|
|
||||||
max-width: 500px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-end-screen-overlay {
|
|
||||||
height: calc(100% - 50px) !important;
|
|
||||||
padding: 25px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-related-video-item {
|
|
||||||
min-height: 80px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
|
||||||
height: calc(100vh - 50px) !important;
|
|
||||||
padding: 60px 25px 25px 25px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure controls stay visible over the black background */
|
|
||||||
.video-js.vjs-ended .vjs-control-bar {
|
|
||||||
z-index: 10000 !important;
|
|
||||||
position: absolute !important;
|
|
||||||
bottom: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
display: flex !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
visibility: visible !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-js.vjs-ended .vjs-progress-control {
|
|
||||||
z-index: 10001 !important;
|
|
||||||
position: absolute !important;
|
|
||||||
bottom: 48px !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
display: block !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
visibility: visible !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Embed-specific controls handling when ended */
|
|
||||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-control-bar {
|
|
||||||
position: fixed !important;
|
|
||||||
bottom: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
z-index: 10000 !important;
|
|
||||||
display: flex !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
visibility: visible !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-progress-control {
|
|
||||||
position: fixed !important;
|
|
||||||
bottom: 48px !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
z-index: 10001 !important;
|
|
||||||
display: block !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
visibility: visible !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure embed info overlay (title/avatar) stays visible when ended */
|
|
||||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-embed-info-overlay {
|
|
||||||
z-index: 10002 !important;
|
|
||||||
display: flex !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
visibility: visible !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide big play button when end screen is active */
|
|
||||||
.video-js.vjs-ended .vjs-big-play-button {
|
|
||||||
display: none !important;
|
|
||||||
opacity: 0 !important;
|
|
||||||
visibility: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide seek indicator (play icon) when end screen is active */
|
|
||||||
.video-js.vjs-ended .vjs-seek-indicator {
|
|
||||||
display: none !important;
|
|
||||||
opacity: 0 !important;
|
|
||||||
visibility: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Make control bar and seekbar background black when video ends */
|
|
||||||
.video-js.vjs-ended .vjs-control-bar {
|
|
||||||
background: #000000 !important;
|
|
||||||
background-color: #000000 !important;
|
|
||||||
background-image: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-js.vjs-ended .vjs-progress-control {
|
|
||||||
background: #000000 !important;
|
|
||||||
background-color: #000000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Also ensure the gradient overlay is black when ended */
|
|
||||||
.video-js.vjs-ended::after {
|
|
||||||
background: #000000 !important;
|
|
||||||
background-image: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove any white elements or gradients */
|
|
||||||
.video-js.vjs-ended::before {
|
|
||||||
background: #000000 !important;
|
|
||||||
background-image: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure all VideoJS overlays are black but preserve seekbar colors */
|
|
||||||
.video-js.vjs-ended .vjs-loading-spinner,
|
|
||||||
.video-js.vjs-ended .vjs-mouse-display {
|
|
||||||
background: #000000 !important;
|
|
||||||
background-image: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Only change the background holder, preserve progress colors */
|
|
||||||
.video-js.vjs-ended .vjs-progress-holder {
|
|
||||||
background: rgba(255, 255, 255, 0.3) !important; /* Keep original transparent background */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide any remaining VideoJS elements that might show white */
|
|
||||||
.video-js.vjs-ended .vjs-tech,
|
|
||||||
.video-js.vjs-ended .vjs-poster-overlay {
|
|
||||||
display: none !important;
|
|
||||||
opacity: 0 !important;
|
|
||||||
visibility: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-related-videos-title {
|
|
||||||
color: white;
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 24px;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: bold;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-related-videos-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
margin: 0; /* Remove margin since parent handles centering */
|
|
||||||
box-sizing: border-box;
|
|
||||||
justify-items: stretch;
|
|
||||||
align-items: stretch;
|
|
||||||
justify-content: center;
|
|
||||||
align-content: center; /* Center grid content */
|
|
||||||
overflow: hidden;
|
|
||||||
grid-auto-rows: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-related-video-item {
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
overflow: hidden;
|
|
||||||
transition:
|
|
||||||
transform 0.2s ease,
|
|
||||||
box-shadow 0.2s ease;
|
|
||||||
background: #1a1a1a;
|
|
||||||
border: 1px solid #333;
|
|
||||||
aspect-ratio: 16/9;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Apply rounded corners only when useRoundedCorners is true */
|
|
||||||
.video-js.video-js-rounded-corners .vjs-related-video-item {
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-related-video-item:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-related-video-thumbnail {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
display: block;
|
|
||||||
/* border-radius: 8px; */
|
|
||||||
background: #1a1a1a; /* Fallback background */
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-related-video-item:hover .vjs-related-video-thumbnail {
|
|
||||||
transform: scale(1.02); /* Subtle zoom like YouTube */
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-related-video-overlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.9));
|
|
||||||
color: white;
|
|
||||||
padding: 12px;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-related-video-item:hover .vjs-related-video-overlay {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Show overlay by default on touch devices - match default hover behavior exactly */
|
|
||||||
.vjs-related-video-item.vjs-touch-device .vjs-related-video-overlay {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-related-video-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
line-height: 1.3;
|
|
||||||
color: white;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-related-video-meta {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-related-video-author {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-related-video-views {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-related-video-author::after {
|
|
||||||
content: "•";
|
|
||||||
margin-left: 8px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-related-video-duration {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 8px;
|
|
||||||
right: 8px;
|
|
||||||
background: rgba(0, 0, 0, 0.8);
|
|
||||||
color: white;
|
|
||||||
padding: 2px 6px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: bold;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Apply rounded corners to duration badge only when useRoundedCorners is true */
|
|
||||||
.video-js.video-js-rounded-corners .vjs-related-video-duration {
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-related-video-item:hover .vjs-related-video-duration {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Show duration by default on touch devices */
|
|
||||||
.vjs-related-video-item.vjs-touch-device .vjs-related-video-duration {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-js.vjs-ended .vjs-control-bar {
|
|
||||||
opacity: 1 !important;
|
|
||||||
pointer-events: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-js.vjs-ended .vjs-control-bar .vjs-control {
|
|
||||||
opacity: 1 !important;
|
|
||||||
pointer-events: auto !important;
|
|
||||||
cursor: pointer !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-js.vjs-ended .vjs-control-bar button {
|
|
||||||
opacity: 1 !important;
|
|
||||||
pointer-events: auto !important;
|
|
||||||
cursor: pointer !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-js.vjs-ended .vjs-control-bar .vjs-control.vjs-volume-control {
|
|
||||||
opacity: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-js.vjs-ended .vjs-control-bar .vjs-volume-panel.vjs-hover .vjs-volume-control {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive grid adjustments for different screen sizes */
|
|
||||||
@media (max-width: 1200px) {
|
|
||||||
.vjs-related-videos-grid {
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-end-screen-overlay {
|
|
||||||
height: calc(100% - 70px);
|
|
||||||
padding: 35px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
|
||||||
height: calc(100vh - 70px) !important;
|
|
||||||
padding: 115px 35px 35px 35px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.vjs-related-videos-grid {
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-end-screen-overlay {
|
|
||||||
height: calc(100% - 60px);
|
|
||||||
padding: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
|
||||||
height: calc(100vh - 60px) !important;
|
|
||||||
padding: 110px 30px 30px 30px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.vjs-related-videos-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-end-screen-overlay {
|
|
||||||
height: calc(100% - 50px);
|
|
||||||
padding: 25px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
|
||||||
height: calc(100vh - 50px) !important;
|
|
||||||
padding: 105px 25px 25px 25px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-related-video-item {
|
|
||||||
min-height: 80px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 400px) {
|
|
||||||
.vjs-related-videos-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-end-screen-overlay {
|
|
||||||
height: calc(100% - 40px);
|
|
||||||
padding: 20px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
|
||||||
height: calc(100vh - 40px) !important;
|
|
||||||
padding: 100px 20px 20px 20px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-related-video-item {
|
|
||||||
min-height: 70px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-js.vjs-ended .vjs-play-control {
|
|
||||||
opacity: 1 !important;
|
|
||||||
pointer-events: auto !important;
|
|
||||||
cursor: pointer !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-js.vjs-ended .vjs-progress-control {
|
|
||||||
opacity: 1 !important;
|
|
||||||
pointer-events: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-js.vjs-ended .vjs-volume-panel {
|
|
||||||
opacity: 1 !important;
|
|
||||||
pointer-events: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive grid layouts */
|
|
||||||
@media (min-width: 1200px) {
|
|
||||||
.vjs-related-videos-grid {
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-end-screen-overlay {
|
|
||||||
height: calc(100% - 80px);
|
|
||||||
padding: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
|
||||||
height: calc(100vh - 80px) !important;
|
|
||||||
padding: 120px 40px 40px 40px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1199px) {
|
|
||||||
.vjs-related-video-item:nth-child(n + 10) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
|
||||||
.vjs-related-videos-grid {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* iPad Pro and larger tablets */
|
|
||||||
@media (min-width: 1024px) and (max-width: 1199px) {
|
|
||||||
.vjs-related-videos-grid {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Allow up to 9 videos on larger tablets */
|
|
||||||
.vjs-related-video-item:nth-child(n + 10) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Large tablets like iPad Pro */
|
|
||||||
@media (min-width: 900px) and (max-width: 1024px) {
|
|
||||||
.vjs-related-videos-grid {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Allow up to 9 videos on large tablets */
|
|
||||||
.vjs-related-video-item:nth-child(n + 10) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) and (max-width: 899px) {
|
|
||||||
.vjs-related-videos-grid {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-end-screen-overlay {
|
|
||||||
height: calc(100% - 60px);
|
|
||||||
padding: 30px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
|
||||||
height: calc(100vh - 60px) !important;
|
|
||||||
padding: 110px 30px 30px 30px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Allow up to 9 videos on regular tablets */
|
|
||||||
.vjs-related-video-item:nth-child(n + 10) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.vjs-related-video-item:nth-child(n + 5) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-related-videos-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
grid-template-rows: repeat(2, 1fr);
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-end-screen-overlay {
|
|
||||||
padding: 12px;
|
|
||||||
justify-content: center;
|
|
||||||
height: calc(100% - 105px);
|
|
||||||
}
|
|
||||||
|
|
||||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
|
||||||
height: calc(100vh - 105px) !important;
|
|
||||||
padding: 80px 12px 12px 12px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-related-video-thumbnail {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 574px) {
|
|
||||||
.vjs-related-video-item:nth-child(n + 5) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-related-videos-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
grid-template-rows: repeat(2, 1fr);
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-end-screen-overlay {
|
|
||||||
padding: 10px;
|
|
||||||
justify-content: center;
|
|
||||||
height: calc(100% - 100px);
|
|
||||||
}
|
|
||||||
|
|
||||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
|
||||||
height: calc(100vh - 100px) !important;
|
|
||||||
padding: 80px 10px 10px 10px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 439px) {
|
|
||||||
.vjs-related-video-item:nth-child(n + 5) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.vjs-related-videos-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
grid-template-rows: repeat(2, 1fr);
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vjs-end-screen-overlay {
|
|
||||||
padding: 8px;
|
|
||||||
justify-content: center;
|
|
||||||
height: calc(100% - 98px);
|
|
||||||
}
|
|
||||||
|
|
||||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
|
||||||
height: calc(100vh - 98px) !important;
|
|
||||||
padding: 80px 8px 8px 8px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.vjs-related-video-thumbnail {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,367 +1,303 @@
|
|||||||
import videojs from 'video.js';
|
import videojs from 'video.js';
|
||||||
import './EndScreenOverlay.css';
|
import './EndScreenOverlay.css';
|
||||||
|
|
||||||
const Component = videojs.getComponent('Component');
|
const Component = videojs.getComponent('Component');
|
||||||
|
|
||||||
class EndScreenOverlay extends Component {
|
class EndScreenOverlay extends Component {
|
||||||
constructor(player, options) {
|
constructor(player, options) {
|
||||||
// Store relatedVideos in options before calling super
|
super(player, options);
|
||||||
// so it's available during createEl()
|
// Safely initialize relatedVideos with multiple fallbacks
|
||||||
if (options && options.relatedVideos) {
|
this.relatedVideos = options?.relatedVideos || options?._relatedVideos || this.options_?.relatedVideos || [];
|
||||||
options._relatedVideos = options.relatedVideos;
|
console.log('relatedVideos1', this.relatedVideos);
|
||||||
|
this.isTouchDevice = this.detectTouchDevice();
|
||||||
|
|
||||||
|
// Bind methods to preserve 'this' context
|
||||||
|
this.getVideosToShow = this.getVideosToShow.bind(this);
|
||||||
|
this.getGridConfig = this.getGridConfig.bind(this);
|
||||||
|
this.createVideoItem = this.createVideoItem.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
super(player, options);
|
// Method to update related videos after initialization
|
||||||
|
setRelatedVideos(videos) {
|
||||||
// Now set the instance property after super() completes
|
this.relatedVideos = videos || [];
|
||||||
this.relatedVideos = options && options.relatedVideos ? options.relatedVideos : [];
|
console.log('Updated relatedVideos:', this.relatedVideos);
|
||||||
}
|
}
|
||||||
|
|
||||||
createEl() {
|
createEl() {
|
||||||
// Get relatedVideos from options since createEl is called during super()
|
|
||||||
const relatedVideos = this.options_ && this.options_._relatedVideos ? this.options_._relatedVideos : [];
|
|
||||||
|
|
||||||
// Limit videos based on screen size to fit grid properly
|
|
||||||
const maxVideos = this.getMaxVideosForScreen();
|
|
||||||
const videosToShow = relatedVideos.slice(0, maxVideos);
|
|
||||||
|
|
||||||
// Determine if player is small and add appropriate class
|
|
||||||
const playerEl = this.player().el();
|
|
||||||
const playerWidth = playerEl ? playerEl.offsetWidth : window.innerWidth;
|
|
||||||
const playerHeight = playerEl ? playerEl.offsetHeight : window.innerHeight;
|
|
||||||
const isSmallPlayer = playerHeight <= 500 || playerWidth <= 600;
|
|
||||||
const isVerySmallPlayer = playerHeight <= 400 || playerWidth <= 400;
|
|
||||||
|
|
||||||
let overlayClasses = 'vjs-end-screen-overlay';
|
|
||||||
if (isVerySmallPlayer) {
|
|
||||||
overlayClasses += ' vjs-very-small-player vjs-small-player';
|
|
||||||
} else if (isSmallPlayer) {
|
|
||||||
overlayClasses += ' vjs-small-player';
|
|
||||||
}
|
|
||||||
|
|
||||||
const overlay = super.createEl('div', {
|
const overlay = super.createEl('div', {
|
||||||
className: overlayClasses,
|
className: 'vjs-end-screen-overlay',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create grid container
|
// Position overlay above control bar
|
||||||
const grid = videojs.dom.createEl('div', {
|
overlay.style.position = 'absolute';
|
||||||
className: 'vjs-related-videos-grid',
|
overlay.style.top = '0';
|
||||||
});
|
overlay.style.left = '0';
|
||||||
|
overlay.style.right = '0';
|
||||||
// Create video items
|
overlay.style.bottom = '60px'; // Leave space for control bar
|
||||||
if (videosToShow && Array.isArray(videosToShow) && videosToShow.length > 0) {
|
overlay.style.display = 'none'; // Hidden by default
|
||||||
videosToShow.forEach((video) => {
|
overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
|
||||||
const videoItem = this.createVideoItem(video);
|
overlay.style.zIndex = '100';
|
||||||
grid.appendChild(videoItem);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Create sample videos for testing if no related videos provided
|
|
||||||
const sampleVideos = this.createSampleVideos();
|
|
||||||
sampleVideos.slice(0, this.getMaxVideosForScreen()).forEach((video) => {
|
|
||||||
const videoItem = this.createVideoItem(video);
|
|
||||||
grid.appendChild(videoItem);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Create responsive grid
|
||||||
|
const grid = this.createGrid();
|
||||||
overlay.appendChild(grid);
|
overlay.appendChild(grid);
|
||||||
|
|
||||||
return overlay;
|
return overlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
createVideoItem(video) {
|
createGrid() {
|
||||||
// Detect touch device
|
const grid = videojs.dom.createEl('div', {
|
||||||
const isTouchDevice = this.isTouchDevice();
|
className: 'vjs-related-videos-grid',
|
||||||
|
|
||||||
const item = videojs.dom.createEl('div', {
|
|
||||||
className: isTouchDevice ? 'vjs-related-video-item vjs-touch-device' : 'vjs-related-video-item',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use real YouTube thumbnail or fallback to placeholder
|
// Responsive grid styling
|
||||||
const thumbnailSrc = video.thumbnail || this.getPlaceholderImage(video.title);
|
grid.style.display = 'grid';
|
||||||
|
grid.style.gap = '12px';
|
||||||
|
grid.style.padding = '20px';
|
||||||
|
grid.style.height = '100%';
|
||||||
|
grid.style.overflowY = 'auto';
|
||||||
|
|
||||||
const thumbnail = videojs.dom.createEl('img', {
|
// Responsive grid columns based on player size
|
||||||
className: 'vjs-related-video-thumbnail',
|
const { columns, maxVideos } = this.getGridConfig();
|
||||||
src: thumbnailSrc,
|
grid.style.gridTemplateColumns = `repeat(${columns}, 1fr)`;
|
||||||
alt: video.title,
|
|
||||||
loading: 'lazy', // Lazy load for better performance
|
// Get videos to show - access directly from options during createEl
|
||||||
onerror: () => {
|
const relatedVideos = this.options_?.relatedVideos || this.relatedVideos || [];
|
||||||
// Fallback to placeholder if image fails to load
|
console.log('createGrid relatedVideos:', relatedVideos);
|
||||||
thumbnail.src = this.getPlaceholderImage(video.title);
|
|
||||||
},
|
const videosToShow =
|
||||||
|
relatedVideos.length > 0
|
||||||
|
? relatedVideos.slice(0, maxVideos)
|
||||||
|
: this.createSampleVideos().slice(0, maxVideos);
|
||||||
|
|
||||||
|
// Create video items
|
||||||
|
videosToShow.forEach((video) => {
|
||||||
|
const videoItem = this.createVideoItem(video);
|
||||||
|
grid.appendChild(videoItem);
|
||||||
});
|
});
|
||||||
|
|
||||||
const overlay = videojs.dom.createEl('div', {
|
return grid;
|
||||||
className: 'vjs-related-video-overlay',
|
|
||||||
});
|
|
||||||
|
|
||||||
const title = videojs.dom.createEl('div', {
|
|
||||||
className: 'vjs-related-video-title',
|
|
||||||
});
|
|
||||||
title.textContent = video.title;
|
|
||||||
|
|
||||||
// Create meta container for author and views
|
|
||||||
const meta = videojs.dom.createEl('div', {
|
|
||||||
className: 'vjs-related-video-meta',
|
|
||||||
});
|
|
||||||
|
|
||||||
const author = videojs.dom.createEl('div', {
|
|
||||||
className: 'vjs-related-video-author',
|
|
||||||
});
|
|
||||||
author.textContent = video.author;
|
|
||||||
|
|
||||||
const views = videojs.dom.createEl('div', {
|
|
||||||
className: 'vjs-related-video-views',
|
|
||||||
});
|
|
||||||
views.textContent = video.views;
|
|
||||||
|
|
||||||
// Add author and views to meta container
|
|
||||||
meta.appendChild(author);
|
|
||||||
meta.appendChild(views);
|
|
||||||
|
|
||||||
// Add duration display (positioned absolutely in bottom right)
|
|
||||||
const duration = videojs.dom.createEl('div', {
|
|
||||||
className: 'vjs-related-video-duration',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Format duration from seconds to MM:SS
|
|
||||||
const formatDuration = (seconds) => {
|
|
||||||
if (!seconds || seconds === 0) return '';
|
|
||||||
const mins = Math.floor(seconds / 60);
|
|
||||||
const secs = Math.floor(seconds % 60);
|
|
||||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
duration.textContent = formatDuration(video.duration);
|
|
||||||
|
|
||||||
// Structure: title at top, meta at bottom
|
|
||||||
overlay.appendChild(title);
|
|
||||||
overlay.appendChild(meta);
|
|
||||||
|
|
||||||
item.appendChild(thumbnail);
|
|
||||||
item.appendChild(overlay);
|
|
||||||
|
|
||||||
// Add duration to the item (positioned absolutely)
|
|
||||||
if (video.duration && video.duration > 0) {
|
|
||||||
item.appendChild(duration);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getGridConfig() {
|
||||||
|
const playerEl = this.player().el();
|
||||||
|
const playerWidth = playerEl?.offsetWidth || window.innerWidth;
|
||||||
|
const playerHeight = playerEl?.offsetHeight || window.innerHeight;
|
||||||
|
|
||||||
|
// Responsive grid configuration
|
||||||
|
if (playerWidth >= 1200) {
|
||||||
|
return { columns: 4, maxVideos: 8 }; // 4x2 grid for large screens
|
||||||
|
} else if (playerWidth >= 800) {
|
||||||
|
return { columns: 3, maxVideos: 6 }; // 3x2 grid for medium screens
|
||||||
|
} else if (playerWidth >= 500) {
|
||||||
|
return { columns: 2, maxVideos: 4 }; // 2x2 grid for small screens
|
||||||
|
} else {
|
||||||
|
return { columns: 1, maxVideos: 3 }; // 1 column for very small screens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getVideosToShow(maxVideos) {
|
||||||
|
// Safely check if relatedVideos exists and has content
|
||||||
|
console.log('relatedVideos', this.relatedVideos);
|
||||||
|
if (this.relatedVideos && Array.isArray(this.relatedVideos) && this.relatedVideos.length > 0) {
|
||||||
|
return this.relatedVideos.slice(0, maxVideos);
|
||||||
|
}
|
||||||
|
// Fallback to sample videos for testing
|
||||||
|
return this.createSampleVideos().slice(0, maxVideos);
|
||||||
|
}
|
||||||
|
|
||||||
|
createVideoItem(video) {
|
||||||
|
const item = videojs.dom.createEl('div', {
|
||||||
|
className: 'vjs-related-video-item',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Item styling
|
||||||
|
item.style.position = 'relative';
|
||||||
|
item.style.backgroundColor = '#1a1a1a';
|
||||||
|
item.style.borderRadius = '8px';
|
||||||
|
item.style.overflow = 'hidden';
|
||||||
|
item.style.cursor = 'pointer';
|
||||||
|
item.style.transition = 'transform 0.2s ease, box-shadow 0.2s ease';
|
||||||
|
|
||||||
|
// Hover/touch effects
|
||||||
|
if (this.isTouchDevice) {
|
||||||
|
item.style.touchAction = 'manipulation';
|
||||||
|
} else {
|
||||||
|
item.addEventListener('mouseenter', () => {
|
||||||
|
item.style.transform = 'scale(1.05)';
|
||||||
|
item.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.3)';
|
||||||
|
});
|
||||||
|
item.addEventListener('mouseleave', () => {
|
||||||
|
item.style.transform = 'scale(1)';
|
||||||
|
item.style.boxShadow = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create thumbnail
|
||||||
|
const thumbnail = this.createThumbnail(video);
|
||||||
|
item.appendChild(thumbnail);
|
||||||
|
|
||||||
|
// Create info overlay
|
||||||
|
const info = this.createVideoInfo(video);
|
||||||
|
item.appendChild(info);
|
||||||
|
|
||||||
// Add click handler
|
// Add click handler
|
||||||
item.addEventListener('click', () => {
|
this.addClickHandler(item, video);
|
||||||
// Check if this is an embed player - use multiple methods for reliability
|
|
||||||
const playerId = this.player().id() || this.player().options_.id;
|
|
||||||
const isEmbedPlayer =
|
|
||||||
playerId === 'video-embed' ||
|
|
||||||
window.location.pathname.includes('/embed') ||
|
|
||||||
window.location.search.includes('embed') ||
|
|
||||||
window.parent !== window; // Most reliable check for iframe
|
|
||||||
|
|
||||||
if (isEmbedPlayer) {
|
|
||||||
// Open in new tab/window for embed players
|
|
||||||
window.open(`/view?m=${video.id}`, '_blank', 'noopener,noreferrer');
|
|
||||||
} else {
|
|
||||||
// Navigate in same window for regular players
|
|
||||||
window.location.href = `/view?m=${video.id}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createThumbnail(video) {
|
||||||
|
const thumbnail = videojs.dom.createEl('img', {
|
||||||
|
className: 'vjs-related-video-thumbnail',
|
||||||
|
src: video.thumbnail || this.getPlaceholderImage(video.title),
|
||||||
|
alt: video.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Thumbnail styling
|
||||||
|
thumbnail.style.width = '100%';
|
||||||
|
thumbnail.style.height = '120px';
|
||||||
|
thumbnail.style.objectFit = 'cover';
|
||||||
|
thumbnail.style.display = 'block';
|
||||||
|
|
||||||
|
// Add duration badge if available
|
||||||
|
if (video.duration && video.duration > 0) {
|
||||||
|
const duration = videojs.dom.createEl('div', {
|
||||||
|
className: 'vjs-video-duration',
|
||||||
|
});
|
||||||
|
duration.textContent = this.formatDuration(video.duration);
|
||||||
|
duration.style.position = 'absolute';
|
||||||
|
duration.style.bottom = '50px';
|
||||||
|
duration.style.right = '8px';
|
||||||
|
duration.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
|
||||||
|
duration.style.color = 'white';
|
||||||
|
duration.style.padding = '2px 6px';
|
||||||
|
duration.style.borderRadius = '4px';
|
||||||
|
duration.style.fontSize = '12px';
|
||||||
|
duration.style.fontWeight = 'bold';
|
||||||
|
|
||||||
|
// Add duration to parent item (will be added later)
|
||||||
|
thumbnail.durationBadge = duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
return thumbnail;
|
||||||
|
}
|
||||||
|
|
||||||
|
createVideoInfo(video) {
|
||||||
|
const info = videojs.dom.createEl('div', {
|
||||||
|
className: 'vjs-related-video-info',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Info styling
|
||||||
|
info.style.padding = '12px';
|
||||||
|
info.style.color = 'white';
|
||||||
|
|
||||||
|
// Title
|
||||||
|
const title = videojs.dom.createEl('div', {
|
||||||
|
className: 'vjs-related-video-title',
|
||||||
|
});
|
||||||
|
title.textContent = video.title;
|
||||||
|
title.style.fontSize = '14px';
|
||||||
|
title.style.fontWeight = 'bold';
|
||||||
|
title.style.marginBottom = '4px';
|
||||||
|
title.style.lineHeight = '1.3';
|
||||||
|
title.style.overflow = 'hidden';
|
||||||
|
title.style.textOverflow = 'ellipsis';
|
||||||
|
title.style.display = '-webkit-box';
|
||||||
|
title.style.webkitLineClamp = '2';
|
||||||
|
title.style.webkitBoxOrient = 'vertical';
|
||||||
|
|
||||||
|
// Author and views
|
||||||
|
const meta = videojs.dom.createEl('div', {
|
||||||
|
className: 'vjs-related-video-meta',
|
||||||
|
});
|
||||||
|
meta.textContent = `${video.author} • ${video.views}`;
|
||||||
|
meta.style.fontSize = '12px';
|
||||||
|
meta.style.color = '#aaa';
|
||||||
|
meta.style.overflow = 'hidden';
|
||||||
|
meta.style.textOverflow = 'ellipsis';
|
||||||
|
meta.style.whiteSpace = 'nowrap';
|
||||||
|
|
||||||
|
info.appendChild(title);
|
||||||
|
info.appendChild(meta);
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
addClickHandler(item, video) {
|
||||||
|
const clickHandler = () => {
|
||||||
|
const isEmbedPlayer = this.player().id() === 'video-embed' || window.parent !== window;
|
||||||
|
|
||||||
|
if (isEmbedPlayer) {
|
||||||
|
window.open(`/view?m=${video.id}`, '_blank', 'noopener,noreferrer');
|
||||||
|
} else {
|
||||||
|
window.location.href = `/view?m=${video.id}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.isTouchDevice) {
|
||||||
|
item.addEventListener('touchend', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
clickHandler();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
item.addEventListener('click', clickHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add duration badge if it exists
|
||||||
|
if (item.querySelector('img').durationBadge) {
|
||||||
|
item.appendChild(item.querySelector('img').durationBadge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDuration(seconds) {
|
||||||
|
if (!seconds || seconds === 0) return '';
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = Math.floor(seconds % 60);
|
||||||
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
getPlaceholderImage(title) {
|
getPlaceholderImage(title) {
|
||||||
// Generate a placeholder image using a service or create a data URL
|
const colors = ['#009931', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'];
|
||||||
// For now, we'll use a simple colored placeholder based on the title
|
|
||||||
const colors = [
|
|
||||||
'#009931',
|
|
||||||
'#4ECDC4',
|
|
||||||
'#45B7D1',
|
|
||||||
'#96CEB4',
|
|
||||||
'#FFEAA7',
|
|
||||||
'#DDA0DD',
|
|
||||||
'#98D8C8',
|
|
||||||
'#F7DC6F',
|
|
||||||
'#BB8FCE',
|
|
||||||
'#85C1E9',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Use title hash to consistently assign colors
|
// Use title hash to consistently assign colors
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
for (let i = 0; i < title.length; i++) {
|
for (let i = 0; i < title.length; i++) {
|
||||||
hash = title.charCodeAt(i) + ((hash << 5) - hash);
|
hash = title.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
}
|
}
|
||||||
const colorIndex = Math.abs(hash) % colors.length;
|
const color = colors[Math.abs(hash) % colors.length];
|
||||||
const color = colors[colorIndex];
|
|
||||||
|
|
||||||
// Create a simple placeholder with the first letter of the title
|
|
||||||
const firstLetter = title.charAt(0).toUpperCase();
|
const firstLetter = title.charAt(0).toUpperCase();
|
||||||
|
|
||||||
// Create a data URL for a simple placeholder image
|
// Create simple SVG placeholder
|
||||||
const canvas = document.createElement('canvas');
|
return `data:image/svg+xml,${encodeURIComponent(`
|
||||||
canvas.width = 320;
|
<svg width="320" height="180" xmlns="http://www.w3.org/2000/svg">
|
||||||
canvas.height = 180;
|
<rect width="320" height="180" fill="${color}"/>
|
||||||
const ctx = canvas.getContext('2d');
|
<text x="160" y="90" font-family="Arial" font-size="48" font-weight="bold"
|
||||||
|
text-anchor="middle" dominant-baseline="middle" fill="white">${firstLetter}</text>
|
||||||
// Background
|
</svg>
|
||||||
ctx.fillStyle = color;
|
`)}`;
|
||||||
ctx.fillRect(0, 0, 320, 180);
|
|
||||||
|
|
||||||
// Add a subtle pattern
|
|
||||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
|
|
||||||
for (let i = 0; i < 20; i++) {
|
|
||||||
ctx.fillRect(Math.random() * 320, Math.random() * 180, 2, 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the first letter
|
detectTouchDevice() {
|
||||||
ctx.fillStyle = 'white';
|
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||||
ctx.font = 'bold 48px Arial';
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.textBaseline = 'middle';
|
|
||||||
ctx.fillText(firstLetter, 160, 90);
|
|
||||||
|
|
||||||
return canvas.toDataURL();
|
|
||||||
}
|
|
||||||
|
|
||||||
getMaxVideosForScreen() {
|
|
||||||
// Get actual player dimensions instead of window dimensions
|
|
||||||
const playerEl = this.player().el();
|
|
||||||
const playerWidth = playerEl ? playerEl.offsetWidth : window.innerWidth;
|
|
||||||
const playerHeight = playerEl ? playerEl.offsetHeight : window.innerHeight;
|
|
||||||
|
|
||||||
// Check if this is an embed player
|
|
||||||
const playerId = this.player().id() || this.player().options_.id;
|
|
||||||
const isEmbedPlayer =
|
|
||||||
playerId === 'video-embed' ||
|
|
||||||
document.getElementById('page-embed') ||
|
|
||||||
window.location.pathname.includes('embed');
|
|
||||||
|
|
||||||
// For small player sizes, limit to 2 items for better readability
|
|
||||||
// This works for both embed and regular players when they're small
|
|
||||||
if (playerHeight <= 500 || playerWidth <= 600) {
|
|
||||||
return 2; // 2x1 grid for small player sizes
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use player width for responsive decisions
|
|
||||||
if (playerWidth >= 1200) {
|
|
||||||
return 12; // 4x3 grid for large player
|
|
||||||
} else if (playerWidth >= 1024) {
|
|
||||||
return 9; // 3x3 grid for desktop-sized player
|
|
||||||
} else if (playerWidth >= 768) {
|
|
||||||
return 6; // 3x2 grid for tablet-sized player
|
|
||||||
} else {
|
|
||||||
return 4; // 2x2 grid for mobile-sized player
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createSampleVideos() {
|
createSampleVideos() {
|
||||||
return [
|
return [
|
||||||
{
|
{ id: 'sample1', title: 'React Full Course', author: 'Bro Code', views: '2.1M views', duration: 1800 },
|
||||||
id: 'sample1',
|
{ id: 'sample2', title: 'JavaScript ES6+', author: 'Tech Tutorials', views: '850K views', duration: 1200 },
|
||||||
title: 'React Full Course for Beginners',
|
{ id: 'sample3', title: 'CSS Grid Layout', author: 'Web Dev Academy', views: '1.2M views', duration: 2400 },
|
||||||
author: 'Bro Code',
|
{ id: 'sample4', title: 'Node.js Backend', author: 'Code Master', views: '650K views', duration: 3600 },
|
||||||
views: '2.1M views',
|
{ id: 'sample5', title: 'Vue.js Guide', author: 'Frontend Pro', views: '980K views', duration: 2800 },
|
||||||
duration: 1800,
|
|
||||||
thumbnail: 'https://img.youtube.com/vi/dGcsHMXbSOA/maxresdefault.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'sample2',
|
|
||||||
title: 'JavaScript ES6+ Features',
|
|
||||||
author: 'Tech Tutorials',
|
|
||||||
views: '850K views',
|
|
||||||
duration: 1200,
|
|
||||||
thumbnail: 'https://img.youtube.com/vi/WZQc7RUAg18/maxresdefault.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'sample3',
|
|
||||||
title: 'CSS Grid Layout Masterclass',
|
|
||||||
author: 'Web Dev Academy',
|
|
||||||
views: '1.2M views',
|
|
||||||
duration: 2400,
|
|
||||||
thumbnail: 'https://img.youtube.com/vi/0xMQfnTU6oo/maxresdefault.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'sample4',
|
|
||||||
title: 'Node.js Backend Development',
|
|
||||||
author: 'Code Master',
|
|
||||||
views: '650K views',
|
|
||||||
duration: 3600,
|
|
||||||
thumbnail: 'https://img.youtube.com/vi/fBNz6F-Cowg/maxresdefault.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'sample5',
|
|
||||||
title: 'Vue.js Complete Guide',
|
|
||||||
author: 'Frontend Pro',
|
|
||||||
views: '980K views',
|
|
||||||
duration: 2800,
|
|
||||||
thumbnail: 'https://img.youtube.com/vi/qZXt1Aom3Cs/maxresdefault.jpg',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'sample6',
|
id: 'sample6',
|
||||||
title: 'Python Data Science',
|
title: 'Python Data Science',
|
||||||
author: 'Data Academy',
|
author: 'Data Academy',
|
||||||
views: '1.5M views',
|
views: '1.5M views',
|
||||||
duration: 4200,
|
duration: 4200,
|
||||||
thumbnail: 'https://img.youtube.com/vi/ua-CiDNNj30/maxresdefault.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'sample7',
|
|
||||||
title: 'TypeScript Fundamentals',
|
|
||||||
author: 'TypeScript Expert',
|
|
||||||
views: '720K views',
|
|
||||||
duration: 2100,
|
|
||||||
thumbnail: 'https://img.youtube.com/vi/BwuLxPH8IDs/maxresdefault.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'sample8',
|
|
||||||
title: 'MongoDB Database Tutorial',
|
|
||||||
author: 'Database Pro',
|
|
||||||
views: '890K views',
|
|
||||||
duration: 1800,
|
|
||||||
thumbnail: 'https://img.youtube.com/vi/-56x56UppqQ/maxresdefault.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'sample9',
|
|
||||||
title: 'Docker Containerization',
|
|
||||||
author: 'DevOps Master',
|
|
||||||
views: '1.1M views',
|
|
||||||
duration: 3200,
|
|
||||||
thumbnail: 'https://img.youtube.com/vi/pTFZFxd4hOI/maxresdefault.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'sample10',
|
|
||||||
title: 'AWS Cloud Services',
|
|
||||||
author: 'Cloud Expert',
|
|
||||||
views: '1.3M views',
|
|
||||||
duration: 4500,
|
|
||||||
thumbnail: 'https://img.youtube.com/vi/ITcXLS3h2qU/maxresdefault.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'sample11',
|
|
||||||
title: 'GraphQL API Design',
|
|
||||||
author: 'API Specialist',
|
|
||||||
views: '680K views',
|
|
||||||
duration: 2600,
|
|
||||||
thumbnail: 'https://img.youtube.com/vi/ed8SzALpx1Q/maxresdefault.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'sample12',
|
|
||||||
title: 'Machine Learning Basics',
|
|
||||||
author: 'AI Academy',
|
|
||||||
views: '2.3M views',
|
|
||||||
duration: 5400,
|
|
||||||
thumbnail: 'https://img.youtube.com/vi/i_LwzRVP7bg/maxresdefault.jpg',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
isTouchDevice() {
|
|
||||||
// Multiple methods to detect touch devices
|
|
||||||
return (
|
|
||||||
'ontouchstart' in window ||
|
|
||||||
navigator.maxTouchPoints > 0 ||
|
|
||||||
navigator.msMaxTouchPoints > 0 ||
|
|
||||||
window.matchMedia('(pointer: coarse)').matches
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
this.el().style.display = 'flex';
|
this.el().style.display = 'flex';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,689 @@
|
|||||||
|
/* ===== END SCREEN OVERLAY STYLES ===== */
|
||||||
|
|
||||||
|
.vjs-end-screen-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 80px); /* Reduce reserved space for seekbar */
|
||||||
|
background: #000000;
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center; /* Center the grid vertically */
|
||||||
|
align-items: center;
|
||||||
|
padding: 40px 40px 40px 40px; /* Equal visual margins on all sides */
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 9999;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide poster image when video ends and end screen is shown */
|
||||||
|
.video-js.vjs-ended .vjs-poster {
|
||||||
|
display: none !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
z-index: -1 !important;
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide video element completely when ended */
|
||||||
|
.video-js.vjs-ended video {
|
||||||
|
display: none !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure the overlay covers everything with maximum z-index */
|
||||||
|
.video-js.vjs-ended .vjs-end-screen-overlay {
|
||||||
|
z-index: 99999 !important;
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Embed-specific full page overlay */
|
||||||
|
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
width: 100vw !important;
|
||||||
|
height: calc(100vh - 80px) !important; /* Reduce reserved space for controls */
|
||||||
|
z-index: 9998 !important; /* Below controls but above video */
|
||||||
|
display: flex !important;
|
||||||
|
padding: 120px 40px 40px 40px !important; /* Top padding for embed info + equal visual margins */
|
||||||
|
justify-content: center !important; /* Center the grid vertically */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small player size optimization - 2 items horizontally for better title readability */
|
||||||
|
/* This applies to both embed and regular players when they're small */
|
||||||
|
.vjs-end-screen-overlay.vjs-small-player .vjs-related-videos-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr) !important;
|
||||||
|
grid-template-rows: 1fr !important;
|
||||||
|
gap: 20px !important;
|
||||||
|
max-width: 600px; /* Limit width for better proportions */
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-end-screen-overlay.vjs-small-player {
|
||||||
|
height: calc(100% - 60px) !important;
|
||||||
|
padding: 30px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide items beyond the first 2 for small players */
|
||||||
|
.vjs-end-screen-overlay.vjs-small-player .vjs-related-video-item:nth-child(n + 3) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Embed-specific adjustments for small sizes */
|
||||||
|
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay.vjs-small-player {
|
||||||
|
height: calc(100vh - 60px) !important;
|
||||||
|
padding: 80px 30px 30px 30px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback media query for cases where class detection might not work */
|
||||||
|
@media (max-height: 500px), (max-width: 600px) {
|
||||||
|
.vjs-related-videos-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr) !important;
|
||||||
|
grid-template-rows: 1fr !important;
|
||||||
|
gap: 20px !important;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-end-screen-overlay {
|
||||||
|
height: calc(100% - 60px) !important;
|
||||||
|
padding: 30px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-related-video-item:nth-child(n + 3) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||||
|
height: calc(100vh - 60px) !important;
|
||||||
|
padding: 80px 30px 30px 30px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Very small player size - further optimize spacing (class-based detection) */
|
||||||
|
.vjs-end-screen-overlay.vjs-very-small-player .vjs-related-videos-grid {
|
||||||
|
gap: 15px !important;
|
||||||
|
max-width: 500px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-end-screen-overlay.vjs-very-small-player {
|
||||||
|
height: calc(100% - 50px) !important;
|
||||||
|
padding: 25px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-end-screen-overlay.vjs-very-small-player .vjs-related-video-item {
|
||||||
|
min-height: 80px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay.vjs-very-small-player {
|
||||||
|
height: calc(100vh - 50px) !important;
|
||||||
|
padding: 60px 25px 25px 25px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback media query for very small sizes */
|
||||||
|
@media (max-height: 400px), (max-width: 400px) {
|
||||||
|
.vjs-related-videos-grid {
|
||||||
|
gap: 15px !important;
|
||||||
|
max-width: 500px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-end-screen-overlay {
|
||||||
|
height: calc(100% - 50px) !important;
|
||||||
|
padding: 25px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-related-video-item {
|
||||||
|
min-height: 80px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||||
|
height: calc(100vh - 50px) !important;
|
||||||
|
padding: 60px 25px 25px 25px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure controls stay visible over the black background */
|
||||||
|
.video-js.vjs-ended .vjs-control-bar {
|
||||||
|
z-index: 10000 !important;
|
||||||
|
position: absolute !important;
|
||||||
|
bottom: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
display: flex !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.vjs-ended .vjs-progress-control {
|
||||||
|
z-index: 10001 !important;
|
||||||
|
position: absolute !important;
|
||||||
|
bottom: 48px !important;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
display: block !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Embed-specific controls handling when ended */
|
||||||
|
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-control-bar {
|
||||||
|
position: fixed !important;
|
||||||
|
bottom: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
width: 100vw !important;
|
||||||
|
z-index: 10000 !important;
|
||||||
|
display: flex !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-progress-control {
|
||||||
|
position: fixed !important;
|
||||||
|
bottom: 48px !important;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
width: 100vw !important;
|
||||||
|
z-index: 10001 !important;
|
||||||
|
display: block !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure embed info overlay (title/avatar) stays visible when ended */
|
||||||
|
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-embed-info-overlay {
|
||||||
|
z-index: 10002 !important;
|
||||||
|
display: flex !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide big play button when end screen is active */
|
||||||
|
.video-js.vjs-ended .vjs-big-play-button {
|
||||||
|
display: none !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide seek indicator (play icon) when end screen is active */
|
||||||
|
.video-js.vjs-ended .vjs-seek-indicator {
|
||||||
|
display: none !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make control bar and seekbar background black when video ends */
|
||||||
|
.video-js.vjs-ended .vjs-control-bar {
|
||||||
|
background: #000000 !important;
|
||||||
|
background-color: #000000 !important;
|
||||||
|
background-image: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.vjs-ended .vjs-progress-control {
|
||||||
|
background: #000000 !important;
|
||||||
|
background-color: #000000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Also ensure the gradient overlay is black when ended */
|
||||||
|
.video-js.vjs-ended::after {
|
||||||
|
background: #000000 !important;
|
||||||
|
background-image: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove any white elements or gradients */
|
||||||
|
.video-js.vjs-ended::before {
|
||||||
|
background: #000000 !important;
|
||||||
|
background-image: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure all VideoJS overlays are black but preserve seekbar colors */
|
||||||
|
.video-js.vjs-ended .vjs-loading-spinner,
|
||||||
|
.video-js.vjs-ended .vjs-mouse-display {
|
||||||
|
background: #000000 !important;
|
||||||
|
background-image: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Only change the background holder, preserve progress colors */
|
||||||
|
.video-js.vjs-ended .vjs-progress-holder {
|
||||||
|
background: rgba(255, 255, 255, 0.3) !important; /* Keep original transparent background */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide any remaining VideoJS elements that might show white */
|
||||||
|
.video-js.vjs-ended .vjs-tech,
|
||||||
|
.video-js.vjs-ended .vjs-poster-overlay {
|
||||||
|
display: none !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-related-videos-title {
|
||||||
|
color: white;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-related-videos-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0; /* Remove margin since parent handles centering */
|
||||||
|
box-sizing: border-box;
|
||||||
|
justify-items: stretch;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: center;
|
||||||
|
align-content: center; /* Center grid content */
|
||||||
|
overflow: hidden;
|
||||||
|
grid-auto-rows: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-related-video-item {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
transform 0.2s ease,
|
||||||
|
box-shadow 0.2s ease;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Apply rounded corners only when useRoundedCorners is true */
|
||||||
|
.video-js.video-js-rounded-corners .vjs-related-video-item {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-related-video-item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-related-video-thumbnail {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
/* border-radius: 8px; */
|
||||||
|
background: #1a1a1a; /* Fallback background */
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-related-video-item:hover .vjs-related-video-thumbnail {
|
||||||
|
transform: scale(1.02); /* Subtle zoom like YouTube */
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-related-video-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(transparent, rgba(0, 0, 0, 0.9));
|
||||||
|
color: white;
|
||||||
|
padding: 12px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-related-video-item:hover .vjs-related-video-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show overlay by default on touch devices - match default hover behavior exactly */
|
||||||
|
.vjs-related-video-item.vjs-touch-device .vjs-related-video-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-related-video-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-related-video-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-related-video-author {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-related-video-views {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-related-video-author::after {
|
||||||
|
content: "•";
|
||||||
|
margin-left: 8px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-related-video-duration {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
right: 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
color: white;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Apply rounded corners to duration badge only when useRoundedCorners is true */
|
||||||
|
.video-js.video-js-rounded-corners .vjs-related-video-duration {
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-related-video-item:hover .vjs-related-video-duration {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show duration by default on touch devices */
|
||||||
|
.vjs-related-video-item.vjs-touch-device .vjs-related-video-duration {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.vjs-ended .vjs-control-bar {
|
||||||
|
opacity: 1 !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.vjs-ended .vjs-control-bar .vjs-control {
|
||||||
|
opacity: 1 !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.vjs-ended .vjs-control-bar button {
|
||||||
|
opacity: 1 !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.vjs-ended .vjs-control-bar .vjs-control.vjs-volume-control {
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.vjs-ended .vjs-control-bar .vjs-volume-panel.vjs-hover .vjs-volume-control {
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive grid adjustments for different screen sizes */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.vjs-related-videos-grid {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-end-screen-overlay {
|
||||||
|
height: calc(100% - 70px);
|
||||||
|
padding: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||||
|
height: calc(100vh - 70px) !important;
|
||||||
|
padding: 115px 35px 35px 35px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.vjs-related-videos-grid {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-end-screen-overlay {
|
||||||
|
height: calc(100% - 60px);
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||||
|
height: calc(100vh - 60px) !important;
|
||||||
|
padding: 110px 30px 30px 30px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.vjs-related-videos-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-end-screen-overlay {
|
||||||
|
height: calc(100% - 50px);
|
||||||
|
padding: 25px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||||
|
height: calc(100vh - 50px) !important;
|
||||||
|
padding: 105px 25px 25px 25px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-related-video-item {
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.vjs-related-videos-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-end-screen-overlay {
|
||||||
|
height: calc(100% - 40px);
|
||||||
|
padding: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||||
|
height: calc(100vh - 40px) !important;
|
||||||
|
padding: 100px 20px 20px 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-related-video-item {
|
||||||
|
min-height: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.vjs-ended .vjs-play-control {
|
||||||
|
opacity: 1 !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.vjs-ended .vjs-progress-control {
|
||||||
|
opacity: 1 !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js.vjs-ended .vjs-volume-panel {
|
||||||
|
opacity: 1 !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive grid layouts */
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.vjs-related-videos-grid {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-end-screen-overlay {
|
||||||
|
height: calc(100% - 80px);
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||||
|
height: calc(100vh - 80px) !important;
|
||||||
|
padding: 120px 40px 40px 40px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1199px) {
|
||||||
|
.vjs-related-video-item:nth-child(n + 10) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.vjs-related-videos-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* iPad Pro and larger tablets */
|
||||||
|
@media (min-width: 1024px) and (max-width: 1199px) {
|
||||||
|
.vjs-related-videos-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Allow up to 9 videos on larger tablets */
|
||||||
|
.vjs-related-video-item:nth-child(n + 10) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Large tablets like iPad Pro */
|
||||||
|
@media (min-width: 900px) and (max-width: 1024px) {
|
||||||
|
.vjs-related-videos-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Allow up to 9 videos on large tablets */
|
||||||
|
.vjs-related-video-item:nth-child(n + 10) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) and (max-width: 899px) {
|
||||||
|
.vjs-related-videos-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-end-screen-overlay {
|
||||||
|
height: calc(100% - 60px);
|
||||||
|
padding: 30px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||||
|
height: calc(100vh - 60px) !important;
|
||||||
|
padding: 110px 30px 30px 30px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Allow up to 9 videos on regular tablets */
|
||||||
|
.vjs-related-video-item:nth-child(n + 10) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.vjs-related-video-item:nth-child(n + 5) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-related-videos-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
grid-template-rows: repeat(2, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-end-screen-overlay {
|
||||||
|
padding: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
height: calc(100% - 105px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||||
|
height: calc(100vh - 105px) !important;
|
||||||
|
padding: 80px 12px 12px 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-related-video-thumbnail {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 574px) {
|
||||||
|
.vjs-related-video-item:nth-child(n + 5) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-related-videos-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
grid-template-rows: repeat(2, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-end-screen-overlay {
|
||||||
|
padding: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
height: calc(100% - 100px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||||
|
height: calc(100vh - 100px) !important;
|
||||||
|
padding: 80px 10px 10px 10px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 439px) {
|
||||||
|
.vjs-related-video-item:nth-child(n + 5) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.vjs-related-videos-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
grid-template-rows: repeat(2, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-end-screen-overlay {
|
||||||
|
padding: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
height: calc(100% - 98px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||||
|
height: calc(100vh - 98px) !important;
|
||||||
|
padding: 80px 8px 8px 8px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.vjs-related-video-thumbnail {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,377 @@
|
|||||||
|
import videojs from 'video.js';
|
||||||
|
import './EndScreenOverlay.css';
|
||||||
|
|
||||||
|
const Component = videojs.getComponent('Component');
|
||||||
|
|
||||||
|
class EndScreenOverlay extends Component {
|
||||||
|
constructor(player, options) {
|
||||||
|
// Store relatedVideos in options before calling super
|
||||||
|
// so it's available during createEl()
|
||||||
|
if (options && options.relatedVideos) {
|
||||||
|
options._relatedVideos = options.relatedVideos;
|
||||||
|
}
|
||||||
|
|
||||||
|
super(player, options);
|
||||||
|
|
||||||
|
// Now set the instance property after super() completes
|
||||||
|
this.relatedVideos = options && options.relatedVideos ? options.relatedVideos : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
createEl() {
|
||||||
|
// Get relatedVideos from options since createEl is called during super()
|
||||||
|
const relatedVideos = this.options_ && this.options_._relatedVideos ? this.options_._relatedVideos : [];
|
||||||
|
|
||||||
|
// Limit videos based on screen size to fit grid properly
|
||||||
|
const maxVideos = this.getMaxVideosForScreen();
|
||||||
|
const videosToShow = relatedVideos.slice(0, maxVideos);
|
||||||
|
|
||||||
|
// Determine if player is small and add appropriate class
|
||||||
|
const playerEl = this.player().el();
|
||||||
|
const playerWidth = playerEl ? playerEl.offsetWidth : window.innerWidth;
|
||||||
|
const playerHeight = playerEl ? playerEl.offsetHeight : window.innerHeight;
|
||||||
|
const isSmallPlayer = playerHeight <= 500 || playerWidth <= 600;
|
||||||
|
const isVerySmallPlayer = playerHeight <= 400 || playerWidth <= 400;
|
||||||
|
|
||||||
|
let overlayClasses = 'vjs-end-screen-overlay';
|
||||||
|
if (isVerySmallPlayer) {
|
||||||
|
overlayClasses += ' vjs-very-small-player vjs-small-player';
|
||||||
|
} else if (isSmallPlayer) {
|
||||||
|
overlayClasses += ' vjs-small-player';
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlay = super.createEl('div', {
|
||||||
|
className: overlayClasses,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create grid container
|
||||||
|
const grid = videojs.dom.createEl('div', {
|
||||||
|
className: 'vjs-related-videos-grid',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create video items
|
||||||
|
if (videosToShow && Array.isArray(videosToShow) && videosToShow.length > 0) {
|
||||||
|
videosToShow.forEach((video) => {
|
||||||
|
const videoItem = this.createVideoItem(video);
|
||||||
|
grid.appendChild(videoItem);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Create sample videos for testing if no related videos provided
|
||||||
|
const sampleVideos = this.createSampleVideos();
|
||||||
|
sampleVideos.slice(0, this.getMaxVideosForScreen()).forEach((video) => {
|
||||||
|
const videoItem = this.createVideoItem(video);
|
||||||
|
grid.appendChild(videoItem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay.appendChild(grid);
|
||||||
|
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
createVideoItem(video) {
|
||||||
|
// Detect touch device
|
||||||
|
const isTouchDevice = this.isTouchDevice();
|
||||||
|
|
||||||
|
const item = videojs.dom.createEl('div', {
|
||||||
|
className: isTouchDevice ? 'vjs-related-video-item vjs-touch-device' : 'vjs-related-video-item',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use real YouTube thumbnail or fallback to placeholder
|
||||||
|
const thumbnailSrc = video.thumbnail || this.getPlaceholderImage(video.title);
|
||||||
|
|
||||||
|
const thumbnail = videojs.dom.createEl('img', {
|
||||||
|
className: 'vjs-related-video-thumbnail',
|
||||||
|
src: thumbnailSrc,
|
||||||
|
alt: video.title,
|
||||||
|
loading: 'lazy', // Lazy load for better performance
|
||||||
|
onerror: () => {
|
||||||
|
// Fallback to placeholder if image fails to load
|
||||||
|
thumbnail.src = this.getPlaceholderImage(video.title);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const overlay = videojs.dom.createEl('div', {
|
||||||
|
className: 'vjs-related-video-overlay',
|
||||||
|
});
|
||||||
|
|
||||||
|
const title = videojs.dom.createEl('div', {
|
||||||
|
className: 'vjs-related-video-title',
|
||||||
|
});
|
||||||
|
title.textContent = video.title;
|
||||||
|
|
||||||
|
// Create meta container for author and views
|
||||||
|
const meta = videojs.dom.createEl('div', {
|
||||||
|
className: 'vjs-related-video-meta',
|
||||||
|
});
|
||||||
|
|
||||||
|
const author = videojs.dom.createEl('div', {
|
||||||
|
className: 'vjs-related-video-author',
|
||||||
|
});
|
||||||
|
author.textContent = video.author;
|
||||||
|
|
||||||
|
const views = videojs.dom.createEl('div', {
|
||||||
|
className: 'vjs-related-video-views',
|
||||||
|
});
|
||||||
|
views.textContent = video.views;
|
||||||
|
|
||||||
|
// Add author and views to meta container
|
||||||
|
meta.appendChild(author);
|
||||||
|
meta.appendChild(views);
|
||||||
|
|
||||||
|
// Add duration display (positioned absolutely in bottom right)
|
||||||
|
const duration = videojs.dom.createEl('div', {
|
||||||
|
className: 'vjs-related-video-duration',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Format duration from seconds to MM:SS
|
||||||
|
const formatDuration = (seconds) => {
|
||||||
|
if (!seconds || seconds === 0) return '';
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = Math.floor(seconds % 60);
|
||||||
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
duration.textContent = formatDuration(video.duration);
|
||||||
|
|
||||||
|
// Structure: title at top, meta at bottom
|
||||||
|
overlay.appendChild(title);
|
||||||
|
overlay.appendChild(meta);
|
||||||
|
|
||||||
|
item.appendChild(thumbnail);
|
||||||
|
item.appendChild(overlay);
|
||||||
|
|
||||||
|
// Add duration to the item (positioned absolutely)
|
||||||
|
if (video.duration && video.duration > 0) {
|
||||||
|
item.appendChild(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add click handler
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
// Check if this is an embed player - use multiple methods for reliability
|
||||||
|
const playerId = this.player().id() || this.player().options_.id;
|
||||||
|
const isEmbedPlayer =
|
||||||
|
playerId === 'video-embed' ||
|
||||||
|
window.location.pathname.includes('/embed') ||
|
||||||
|
window.location.search.includes('embed') ||
|
||||||
|
window.parent !== window; // Most reliable check for iframe
|
||||||
|
|
||||||
|
if (isEmbedPlayer) {
|
||||||
|
// Open in new tab/window for embed players
|
||||||
|
window.open(`/view?m=${video.id}`, '_blank', 'noopener,noreferrer');
|
||||||
|
} else {
|
||||||
|
// Navigate in same window for regular players
|
||||||
|
window.location.href = `/view?m=${video.id}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlaceholderImage(title) {
|
||||||
|
// Generate a placeholder image using a service or create a data URL
|
||||||
|
// For now, we'll use a simple colored placeholder based on the title
|
||||||
|
const colors = [
|
||||||
|
'#009931',
|
||||||
|
'#4ECDC4',
|
||||||
|
'#45B7D1',
|
||||||
|
'#96CEB4',
|
||||||
|
'#FFEAA7',
|
||||||
|
'#DDA0DD',
|
||||||
|
'#98D8C8',
|
||||||
|
'#F7DC6F',
|
||||||
|
'#BB8FCE',
|
||||||
|
'#85C1E9',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Use title hash to consistently assign colors
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < title.length; i++) {
|
||||||
|
hash = title.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
}
|
||||||
|
const colorIndex = Math.abs(hash) % colors.length;
|
||||||
|
const color = colors[colorIndex];
|
||||||
|
|
||||||
|
// Create a simple placeholder with the first letter of the title
|
||||||
|
const firstLetter = title.charAt(0).toUpperCase();
|
||||||
|
|
||||||
|
// Create a data URL for a simple placeholder image
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = 320;
|
||||||
|
canvas.height = 180;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
// Background
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.fillRect(0, 0, 320, 180);
|
||||||
|
|
||||||
|
// Add a subtle pattern
|
||||||
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
ctx.fillRect(Math.random() * 320, Math.random() * 180, 2, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the first letter
|
||||||
|
ctx.fillStyle = 'white';
|
||||||
|
ctx.font = 'bold 48px Arial';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText(firstLetter, 160, 90);
|
||||||
|
|
||||||
|
return canvas.toDataURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
getMaxVideosForScreen() {
|
||||||
|
// Get actual player dimensions instead of window dimensions
|
||||||
|
const playerEl = this.player().el();
|
||||||
|
const playerWidth = playerEl ? playerEl.offsetWidth : window.innerWidth;
|
||||||
|
const playerHeight = playerEl ? playerEl.offsetHeight : window.innerHeight;
|
||||||
|
|
||||||
|
// Check if this is an embed player
|
||||||
|
const playerId = this.player().id() || this.player().options_.id;
|
||||||
|
const isEmbedPlayer =
|
||||||
|
playerId === 'video-embed' ||
|
||||||
|
document.getElementById('page-embed') ||
|
||||||
|
window.location.pathname.includes('embed');
|
||||||
|
|
||||||
|
// For small player sizes, limit to 2 items for better readability
|
||||||
|
// This works for both embed and regular players when they're small
|
||||||
|
if (playerHeight <= 500 || playerWidth <= 600) {
|
||||||
|
return 2; // 2x1 grid for small player sizes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use player width for responsive decisions
|
||||||
|
if (playerWidth >= 1200) {
|
||||||
|
return 12; // 4x3 grid for large player
|
||||||
|
} else if (playerWidth >= 1024) {
|
||||||
|
return 9; // 3x3 grid for desktop-sized player
|
||||||
|
} else if (playerWidth >= 768) {
|
||||||
|
return 6; // 3x2 grid for tablet-sized player
|
||||||
|
} else {
|
||||||
|
return 4; // 2x2 grid for mobile-sized player
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createSampleVideos() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'sample1',
|
||||||
|
title: 'React Full Course for Beginners',
|
||||||
|
author: 'Bro Code',
|
||||||
|
views: '2.1M views',
|
||||||
|
duration: 1800,
|
||||||
|
thumbnail: 'https://img.youtube.com/vi/dGcsHMXbSOA/maxresdefault.jpg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sample2',
|
||||||
|
title: 'JavaScript ES6+ Features',
|
||||||
|
author: 'Tech Tutorials',
|
||||||
|
views: '850K views',
|
||||||
|
duration: 1200,
|
||||||
|
thumbnail: 'https://img.youtube.com/vi/WZQc7RUAg18/maxresdefault.jpg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sample3',
|
||||||
|
title: 'CSS Grid Layout Masterclass',
|
||||||
|
author: 'Web Dev Academy',
|
||||||
|
views: '1.2M views',
|
||||||
|
duration: 2400,
|
||||||
|
thumbnail: 'https://img.youtube.com/vi/0xMQfnTU6oo/maxresdefault.jpg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sample4',
|
||||||
|
title: 'Node.js Backend Development',
|
||||||
|
author: 'Code Master',
|
||||||
|
views: '650K views',
|
||||||
|
duration: 3600,
|
||||||
|
thumbnail: 'https://img.youtube.com/vi/fBNz6F-Cowg/maxresdefault.jpg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sample5',
|
||||||
|
title: 'Vue.js Complete Guide',
|
||||||
|
author: 'Frontend Pro',
|
||||||
|
views: '980K views',
|
||||||
|
duration: 2800,
|
||||||
|
thumbnail: 'https://img.youtube.com/vi/qZXt1Aom3Cs/maxresdefault.jpg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sample6',
|
||||||
|
title: 'Python Data Science',
|
||||||
|
author: 'Data Academy',
|
||||||
|
views: '1.5M views',
|
||||||
|
duration: 4200,
|
||||||
|
thumbnail: 'https://img.youtube.com/vi/ua-CiDNNj30/maxresdefault.jpg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sample7',
|
||||||
|
title: 'TypeScript Fundamentals',
|
||||||
|
author: 'TypeScript Expert',
|
||||||
|
views: '720K views',
|
||||||
|
duration: 2100,
|
||||||
|
thumbnail: 'https://img.youtube.com/vi/BwuLxPH8IDs/maxresdefault.jpg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sample8',
|
||||||
|
title: 'MongoDB Database Tutorial',
|
||||||
|
author: 'Database Pro',
|
||||||
|
views: '890K views',
|
||||||
|
duration: 1800,
|
||||||
|
thumbnail: 'https://img.youtube.com/vi/-56x56UppqQ/maxresdefault.jpg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sample9',
|
||||||
|
title: 'Docker Containerization',
|
||||||
|
author: 'DevOps Master',
|
||||||
|
views: '1.1M views',
|
||||||
|
duration: 3200,
|
||||||
|
thumbnail: 'https://img.youtube.com/vi/pTFZFxd4hOI/maxresdefault.jpg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sample10',
|
||||||
|
title: 'AWS Cloud Services',
|
||||||
|
author: 'Cloud Expert',
|
||||||
|
views: '1.3M views',
|
||||||
|
duration: 4500,
|
||||||
|
thumbnail: 'https://img.youtube.com/vi/ITcXLS3h2qU/maxresdefault.jpg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sample11',
|
||||||
|
title: 'GraphQL API Design',
|
||||||
|
author: 'API Specialist',
|
||||||
|
views: '680K views',
|
||||||
|
duration: 2600,
|
||||||
|
thumbnail: 'https://img.youtube.com/vi/ed8SzALpx1Q/maxresdefault.jpg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sample12',
|
||||||
|
title: 'Machine Learning Basics',
|
||||||
|
author: 'AI Academy',
|
||||||
|
views: '2.3M views',
|
||||||
|
duration: 5400,
|
||||||
|
thumbnail: 'https://img.youtube.com/vi/i_LwzRVP7bg/maxresdefault.jpg',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isTouchDevice() {
|
||||||
|
// Multiple methods to detect touch devices
|
||||||
|
return (
|
||||||
|
'ontouchstart' in window ||
|
||||||
|
navigator.maxTouchPoints > 0 ||
|
||||||
|
navigator.msMaxTouchPoints > 0 ||
|
||||||
|
window.matchMedia('(pointer: coarse)').matches
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.el().style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.el().style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the component
|
||||||
|
videojs.registerComponent('EndScreenOverlay', EndScreenOverlay);
|
||||||
|
|
||||||
|
export default EndScreenOverlay;
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import React, { useEffect, useRef, useMemo } from 'react';
|
import React, { useEffect, useRef, useMemo } from 'react';
|
||||||
import videojs from 'video.js';
|
import videojs from 'video.js';
|
||||||
import 'video.js/dist/video-js.css';
|
import 'video.js/dist/video-js.css';
|
||||||
import '../../VideoJS.css';
|
// import '../../VideoJS.css';
|
||||||
import '../../styles/embed.css';
|
import '../../styles/embed.css';
|
||||||
import '../controls/SubtitlesButton.css';
|
//import '../controls/SubtitlesButton.css';
|
||||||
|
|
||||||
// Import the separated components
|
// Import the separated components
|
||||||
import EndScreenOverlay from '../overlays/EndScreenOverlay';
|
import EndScreenOverlay from '../overlays/EndScreenOverlay';
|
||||||
@ -2307,29 +2307,14 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
|
|||||||
// BEGIN: Wrap play button in custom div container
|
// BEGIN: Wrap play button in custom div container
|
||||||
const playButtonEl = playToggle.el();
|
const playButtonEl = playToggle.el();
|
||||||
const playButtonWrapper = document.createElement('div');
|
const playButtonWrapper = document.createElement('div');
|
||||||
playButtonWrapper.className =
|
/* playButtonWrapper.className =
|
||||||
'vjs-play-wrapper vjs-menu-button vjs-menu-button-popup vjs-control vjs-button';
|
'vjs-play-wrapper vjs-menu-button vjs-menu-button-popup vjs-control vjs-button'; */
|
||||||
|
|
||||||
// Insert wrapper before the play button and move play button inside
|
// Insert wrapper before the play button and move play button inside
|
||||||
playButtonEl.parentNode.insertBefore(playButtonWrapper, playButtonEl);
|
// playButtonEl.parentNode.insertBefore(playButtonWrapper, playButtonEl);
|
||||||
playButtonWrapper.appendChild(playButtonEl);
|
// playButtonWrapper.appendChild(playButtonEl);
|
||||||
// END: Wrap play button in custom div container
|
// END: Wrap play button in custom div container
|
||||||
|
|
||||||
// BEGIN: Implement custom time display component
|
|
||||||
const customRemainingTime = new CustomRemainingTime(playerRef.current, {
|
|
||||||
displayNegative: false,
|
|
||||||
customPrefix: '',
|
|
||||||
customSuffix: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Insert it early in control bar - right after play button for priority
|
|
||||||
const playToggleIndex = controlBar.children().indexOf(playToggle);
|
|
||||||
controlBar.addChild(customRemainingTime, {}, playToggleIndex + 1);
|
|
||||||
|
|
||||||
// Store reference for cleanup
|
|
||||||
customComponents.current.customRemainingTime = customRemainingTime;
|
|
||||||
// END: Implement custom time display component
|
|
||||||
|
|
||||||
// BEGIN: Implement custom next video button
|
// BEGIN: Implement custom next video button
|
||||||
if (!isEmbedPlayer && (mediaData?.nextLink || isDevMode)) {
|
if (!isEmbedPlayer && (mediaData?.nextLink || isDevMode)) {
|
||||||
// it seems that the nextLink is not always available, and it is need the this.player().trigger('nextVideo'); from NextVideoButton.js // TODO: remove the 1===1 and the mediaData?.nextLink
|
// it seems that the nextLink is not always available, and it is need the this.player().trigger('nextVideo'); from NextVideoButton.js // TODO: remove the 1===1 and the mediaData?.nextLink
|
||||||
@ -2337,24 +2322,35 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
|
|||||||
nextLink: mediaData.nextLink,
|
nextLink: mediaData.nextLink,
|
||||||
});
|
});
|
||||||
const playToggleIndex = controlBar.children().indexOf(playToggle); // Insert it after play button
|
const playToggleIndex = controlBar.children().indexOf(playToggle); // Insert it after play button
|
||||||
controlBar.addChild(nextVideoButton, {}, playToggleIndex + 2); // After time display
|
controlBar.addChild(nextVideoButton, {}, playToggleIndex + 1); // After time display
|
||||||
|
|
||||||
// Wrap next video button in custom div container
|
// Wrap next video button in custom div container
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
const nextVideoButtonEl = nextVideoButton.el();
|
// const nextVideoButtonEl = nextVideoButton.el();
|
||||||
if (nextVideoButtonEl) {
|
// if (nextVideoButtonEl) {
|
||||||
const nextVideoWrapper = document.createElement('div');
|
// const nextVideoWrapper = document.createElement('div');
|
||||||
nextVideoWrapper.className =
|
// /* nextVideoWrapper.className =
|
||||||
'vjs-next-video-wrapper vjs-menu-button vjs-menu-button-popup vjs-control vjs-button';
|
// 'vjs-next-video-wrapper vjs-menu-button vjs-menu-button-popup vjs-control vjs-button'; */
|
||||||
|
|
||||||
// Insert wrapper before the next video button and move button inside
|
// // Insert wrapper before the next video button and move button inside
|
||||||
nextVideoButtonEl.parentNode.insertBefore(nextVideoWrapper, nextVideoButtonEl);
|
// nextVideoButtonEl.parentNode.insertBefore(nextVideoWrapper, nextVideoButtonEl);
|
||||||
nextVideoWrapper.appendChild(nextVideoButtonEl);
|
// nextVideoWrapper.appendChild(nextVideoButtonEl);
|
||||||
}
|
// }
|
||||||
}, 100); // Small delay to ensure button is fully rendered
|
// }, 2000); // Small delay to ensure button is fully rendered
|
||||||
}
|
}
|
||||||
// END: Implement custom next video button
|
// END: Implement custom next video button
|
||||||
|
|
||||||
|
// BEGIN: Implement custom time display component
|
||||||
|
const customRemainingTime = new CustomRemainingTime(playerRef.current, {
|
||||||
|
displayNegative: false,
|
||||||
|
customPrefix: '',
|
||||||
|
customSuffix: '',
|
||||||
|
});
|
||||||
|
const playToggleIndex = controlBar.children().indexOf(playToggle);
|
||||||
|
controlBar.addChild(customRemainingTime, {}, playToggleIndex + 2);
|
||||||
|
customComponents.current.customRemainingTime = customRemainingTime;
|
||||||
|
// END: Implement custom time display component
|
||||||
|
|
||||||
// BEGIN: Wrap volume panel in custom div container
|
// BEGIN: Wrap volume panel in custom div container
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const volumePanel = controlBar.getChild('volumePanel');
|
const volumePanel = controlBar.getChild('volumePanel');
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,73 @@
|
|||||||
|
/* ===== VIDEO.JS ROUNDED CORNERS STYLES ===== */
|
||||||
|
/* Extracted from VideoJS.css for modular import */
|
||||||
|
|
||||||
|
.video-js-root-main .video-js.video-js-rounded-corners,
|
||||||
|
.video-js-root-main .video-js.video-js-rounded-corners.vjs-has-started,
|
||||||
|
.video-js-root-main .video-js.video-js-rounded-corners.vjs-fullscreen,
|
||||||
|
.video-js-root-main .video-js.video-js-rounded-corners.vjs-paused,
|
||||||
|
.video-js-root-main .video-js.video-js-rounded-corners.vjs-ended,
|
||||||
|
.video-js-root-main .video-js.video-js-rounded-corners.chapters-open {
|
||||||
|
/* background-color: transparent !important; */
|
||||||
|
outline: none !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js-root-main .video-js.video-js-rounded-corners .vjs-poster {
|
||||||
|
border-radius: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js-root-main .video-js.video-js-rounded-corners video {
|
||||||
|
border-radius: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* YouTube-style bottom gradient overlay - covers entire video bottom when controls active */
|
||||||
|
/* .video-js-root-main .video-js.video-js-rounded-corners::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 120px;
|
||||||
|
background: linear-gradient(
|
||||||
|
to top,
|
||||||
|
rgba(0, 0, 0, 0.8) 0%,
|
||||||
|
rgba(0, 0, 0, 0.6) 25%,
|
||||||
|
rgba(0, 0, 0, 0.4) 50%,
|
||||||
|
rgba(0, 0, 0, 0.2) 75%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
z-index: 2;
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* Show overlay when controls are active - YouTube style */
|
||||||
|
.video-js-root-main .video-js.video-js-rounded-corners.vjs-user-active::after,
|
||||||
|
.video-js-root-main .video-js.video-js-rounded-corners.vjs-paused::after,
|
||||||
|
.video-js-root-main .video-js.video-js-rounded-corners.vjs-ended::after {
|
||||||
|
border-bottom-left-radius: 12px !important;
|
||||||
|
border-bottom-right-radius: 12px !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsive adjustments */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
/* Remove rounded corners on mobile screens */
|
||||||
|
.video-js-root-main .video-js.video-js-rounded-corners,
|
||||||
|
.video-js-root-main .video-js.video-js-rounded-corners.vjs-has-started,
|
||||||
|
.video-js-root-main .video-js.video-js-rounded-corners.vjs-fullscreen,
|
||||||
|
.video-js-root-main .video-js.video-js-rounded-corners.vjs-paused,
|
||||||
|
.video-js-root-main .video-js.video-js-rounded-corners.vjs-ended,
|
||||||
|
.video-js-root-main .video-js.video-js-rounded-corners.chapters-open {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js-root-main .video-js.video-js-rounded-corners .vjs-poster {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-js-root-main .video-js.video-js-rounded-corners video {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,29 +1,51 @@
|
|||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import './VideoJS.css';
|
|
||||||
|
|
||||||
import VideoJS from './VideoJS.jsx';
|
import VideoJS from './VideoJS.jsx';
|
||||||
|
import VideoJSNew from './VideoJSNew.jsx';
|
||||||
|
|
||||||
// Mount the components when the DOM is ready
|
// Mount the components when the DOM is ready
|
||||||
const mountComponents = () => {
|
const mountComponents = () => {
|
||||||
// Mount main video player
|
// Mount main video player
|
||||||
const rootContainerMain = document.getElementById('video-js-root-main');
|
const rootContainerMain = document.getElementById('video-js-root-main-old');
|
||||||
if (rootContainerMain && !rootContainerMain.hasChildNodes()) {
|
if (rootContainerMain && !rootContainerMain.hasChildNodes()) {
|
||||||
const rootMain = createRoot(rootContainerMain);
|
const rootMain = createRoot(rootContainerMain);
|
||||||
rootMain.render(
|
rootMain.render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<VideoJS videoId="video-main" />
|
<VideoJS videoId="video-main-old" />
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mount embed video player
|
// Mount embed video player
|
||||||
const rootContainerEmbed = document.getElementById('video-js-root-embed');
|
const rootContainerEmbed = document.getElementById('video-js-root-embed-old');
|
||||||
if (rootContainerEmbed && !rootContainerEmbed.hasChildNodes()) {
|
if (rootContainerEmbed && !rootContainerEmbed.hasChildNodes()) {
|
||||||
const rootEmbed = createRoot(rootContainerEmbed);
|
const rootEmbed = createRoot(rootContainerEmbed);
|
||||||
rootEmbed.render(
|
rootEmbed.render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<VideoJS videoId="video-embed" />
|
<VideoJS videoId="video-embed-old" />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mount main video player
|
||||||
|
const rootContainerMainNew = document.getElementById('video-js-root-main');
|
||||||
|
if (rootContainerMainNew && !rootContainerMainNew.hasChildNodes()) {
|
||||||
|
const rootMain = createRoot(rootContainerMainNew);
|
||||||
|
rootMain.render(
|
||||||
|
<StrictMode>
|
||||||
|
<VideoJSNew videoId="video-main" />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mount embed video player
|
||||||
|
const rootContainerEmbedNew = document.getElementById('video-js-root-embed');
|
||||||
|
if (rootContainerEmbedNew && !rootContainerEmbedNew.hasChildNodes()) {
|
||||||
|
const rootEmbed = createRoot(rootContainerEmbedNew);
|
||||||
|
rootEmbed.render(
|
||||||
|
<StrictMode>
|
||||||
|
<VideoJSNew videoId="video-embed" />
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
87
frontend-tools/video-js/src/utils/AutoplayHandler.js
Normal file
87
frontend-tools/video-js/src/utils/AutoplayHandler.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
export class AutoplayHandler {
|
||||||
|
constructor(player, mediaData, userPreferences) {
|
||||||
|
this.player = player;
|
||||||
|
this.mediaData = mediaData;
|
||||||
|
this.userPreferences = userPreferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasUserInteracted() {
|
||||||
|
return (
|
||||||
|
document.hasFocus() ||
|
||||||
|
document.visibilityState === 'visible' ||
|
||||||
|
sessionStorage.getItem('userInteracted') === 'true'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleAutoplay() {
|
||||||
|
// Don't attempt autoplay if already playing or loading
|
||||||
|
if (!this.player.paused() || this.player.seeking()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define variables outside try block so they're accessible in catch
|
||||||
|
const userInteracted = this.hasUserInteracted();
|
||||||
|
const savedMuteState = this.userPreferences.getPreference('muted');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Respect user's saved mute preference, but try unmuted if user interacted and hasn't explicitly muted
|
||||||
|
if (!this.mediaData.urlMuted && userInteracted && savedMuteState !== true) {
|
||||||
|
this.player.muted(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// First attempt: try to play with current mute state
|
||||||
|
await this.player.play();
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback to muted autoplay unless user explicitly wants to stay unmuted
|
||||||
|
if (!this.player.muted()) {
|
||||||
|
try {
|
||||||
|
this.player.muted(true);
|
||||||
|
await this.player.play();
|
||||||
|
|
||||||
|
// Only try to restore sound if user hasn't explicitly saved mute=true
|
||||||
|
if (savedMuteState !== true) {
|
||||||
|
this.restoreSound(userInteracted);
|
||||||
|
}
|
||||||
|
} catch (mutedError) {
|
||||||
|
console.error('❌ Even muted autoplay was blocked:', mutedError.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreSound(userInteracted) {
|
||||||
|
const restoreSound = () => {
|
||||||
|
if (this.player && !this.player.isDisposed()) {
|
||||||
|
this.player.muted(false);
|
||||||
|
this.player.trigger('notify', '🔊 Sound enabled!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try to restore sound immediately if user has interacted
|
||||||
|
if (userInteracted) {
|
||||||
|
setTimeout(restoreSound, 100);
|
||||||
|
} else {
|
||||||
|
// Show notification for manual interaction
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.player && !this.player.isDisposed()) {
|
||||||
|
this.player.trigger('notify', '🔇 Click anywhere to enable sound');
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// Set up interaction listeners
|
||||||
|
const enableSound = () => {
|
||||||
|
restoreSound();
|
||||||
|
// Mark user interaction for future videos
|
||||||
|
sessionStorage.setItem('userInteracted', 'true');
|
||||||
|
// Remove listeners
|
||||||
|
document.removeEventListener('click', enableSound);
|
||||||
|
document.removeEventListener('keydown', enableSound);
|
||||||
|
document.removeEventListener('touchstart', enableSound);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('click', enableSound, { once: true });
|
||||||
|
document.addEventListener('keydown', enableSound, { once: true });
|
||||||
|
document.addEventListener('touchstart', enableSound, { once: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
206
frontend-tools/video-js/src/utils/EndScreenHandler.js
Normal file
206
frontend-tools/video-js/src/utils/EndScreenHandler.js
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import EndScreenOverlay from '../components/overlays/EndScreenOverlay';
|
||||||
|
import AutoplayCountdownOverlay from '../components/overlays/AutoplayCountdownOverlay';
|
||||||
|
|
||||||
|
export class EndScreenHandler {
|
||||||
|
constructor(player, options) {
|
||||||
|
this.player = player;
|
||||||
|
this.options = options;
|
||||||
|
this.endScreen = null;
|
||||||
|
this.autoplayCountdown = null;
|
||||||
|
|
||||||
|
this.setupEndScreenHandling();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEndScreenHandling() {
|
||||||
|
// Handle video ended event
|
||||||
|
this.player.on('ended', () => {
|
||||||
|
this.handleVideoEnded();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide end screen and autoplay countdown when user wants to replay
|
||||||
|
const hideEndScreenAndStopCountdown = () => {
|
||||||
|
if (this.endScreen) {
|
||||||
|
this.endScreen.hide();
|
||||||
|
}
|
||||||
|
if (this.autoplayCountdown) {
|
||||||
|
this.autoplayCountdown.stopCountdown();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.player.on('play', hideEndScreenAndStopCountdown);
|
||||||
|
this.player.on('seeking', hideEndScreenAndStopCountdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleVideoEnded() {
|
||||||
|
const { isEmbedPlayer, userPreferences, mediaData, currentVideo, relatedVideos, goToNextVideo } = this.options;
|
||||||
|
|
||||||
|
// For embed players, show big play button when video ends
|
||||||
|
if (isEmbedPlayer) {
|
||||||
|
const bigPlayButton = this.player.getChild('bigPlayButton');
|
||||||
|
if (bigPlayButton) {
|
||||||
|
bigPlayButton.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep controls active after video ends
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.player && !this.player.isDisposed()) {
|
||||||
|
const playerEl = this.player.el();
|
||||||
|
if (playerEl) {
|
||||||
|
// Hide poster image when end screen is shown - multiple approaches
|
||||||
|
const posterImage = this.player.getChild('posterImage');
|
||||||
|
if (posterImage) {
|
||||||
|
posterImage.hide();
|
||||||
|
posterImage.el().style.display = 'none';
|
||||||
|
posterImage.el().style.visibility = 'hidden';
|
||||||
|
posterImage.el().style.opacity = '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide all poster elements directly
|
||||||
|
const posterElements = playerEl.querySelectorAll('.vjs-poster');
|
||||||
|
posterElements.forEach((posterEl) => {
|
||||||
|
posterEl.style.display = 'none';
|
||||||
|
posterEl.style.visibility = 'hidden';
|
||||||
|
posterEl.style.opacity = '0';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set player background to dark to match end screen
|
||||||
|
playerEl.style.backgroundColor = '#000';
|
||||||
|
|
||||||
|
// Keep video element visible but ensure it doesn't show poster
|
||||||
|
const videoEl = playerEl.querySelector('video');
|
||||||
|
if (videoEl) {
|
||||||
|
// Remove poster attribute from video element
|
||||||
|
videoEl.removeAttribute('poster');
|
||||||
|
videoEl.style.backgroundColor = '#000';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the visual ended state but ensure controls work
|
||||||
|
const controlBar = this.player.getChild('controlBar');
|
||||||
|
if (controlBar) {
|
||||||
|
controlBar.show();
|
||||||
|
controlBar.el().style.opacity = '1';
|
||||||
|
controlBar.el().style.pointerEvents = 'auto';
|
||||||
|
|
||||||
|
// Style progress bar to match dark end screen background
|
||||||
|
const progressControl = controlBar.getChild('progressControl');
|
||||||
|
if (progressControl) {
|
||||||
|
progressControl.show();
|
||||||
|
progressControl.el().style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
|
||||||
|
|
||||||
|
// Also style the progress holder for seamless look
|
||||||
|
const progressHolder = progressControl.el().querySelector('.vjs-progress-holder');
|
||||||
|
if (progressHolder) {
|
||||||
|
progressHolder.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
// Check if autoplay is enabled and there's a next video
|
||||||
|
const isAutoplayEnabled = userPreferences.getAutoplayPreference();
|
||||||
|
const hasNextVideo = mediaData.nextLink !== null;
|
||||||
|
|
||||||
|
if (!isEmbedPlayer && isAutoplayEnabled && hasNextVideo) {
|
||||||
|
// If it's a playlist, skip countdown and play directly
|
||||||
|
if (currentVideo.isPlayList) {
|
||||||
|
this.cleanupOverlays();
|
||||||
|
goToNextVideo();
|
||||||
|
} else {
|
||||||
|
this.showAutoplayCountdown(relatedVideos, goToNextVideo);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Autoplay disabled or no next video - show regular end screen
|
||||||
|
this.showEndScreen(relatedVideos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showAutoplayCountdown(relatedVideos, goToNextVideo) {
|
||||||
|
// Get next video data for countdown display - find the next video in related videos
|
||||||
|
let nextVideoData = {
|
||||||
|
title: 'Next Video',
|
||||||
|
author: '',
|
||||||
|
duration: 0,
|
||||||
|
thumbnail: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try to find the next video by URL matching or just use the first related video
|
||||||
|
if (relatedVideos.length > 0) {
|
||||||
|
const nextVideo = relatedVideos[0];
|
||||||
|
nextVideoData = {
|
||||||
|
title: nextVideo.title || 'Next Video',
|
||||||
|
author: nextVideo.author || '',
|
||||||
|
duration: nextVideo.duration || 0,
|
||||||
|
thumbnail: nextVideo.thumbnail || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up any existing overlays
|
||||||
|
this.cleanupOverlays();
|
||||||
|
|
||||||
|
// Show autoplay countdown immediately!
|
||||||
|
this.autoplayCountdown = new AutoplayCountdownOverlay(this.player, {
|
||||||
|
nextVideoData: nextVideoData,
|
||||||
|
countdownSeconds: 5,
|
||||||
|
onPlayNext: () => {
|
||||||
|
goToNextVideo();
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
// Hide countdown and show end screen instead
|
||||||
|
if (this.autoplayCountdown) {
|
||||||
|
this.player.removeChild(this.autoplayCountdown);
|
||||||
|
this.autoplayCountdown = null;
|
||||||
|
}
|
||||||
|
this.showEndScreen(relatedVideos);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.player.addChild(this.autoplayCountdown);
|
||||||
|
// Start countdown immediately without any delay
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.autoplayCountdown && !this.autoplayCountdown.isDisposed()) {
|
||||||
|
this.autoplayCountdown.startCountdown();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
showEndScreen(relatedVideos) {
|
||||||
|
// Prevent creating multiple end screens
|
||||||
|
if (this.endScreen) {
|
||||||
|
this.player.removeChild(this.endScreen);
|
||||||
|
this.endScreen = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show end screen with related videos
|
||||||
|
this.endScreen = new EndScreenOverlay(this.player, {
|
||||||
|
relatedVideos: relatedVideos,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also store the data directly on the component as backup and update it
|
||||||
|
this.endScreen.relatedVideos = relatedVideos;
|
||||||
|
if (this.endScreen.setRelatedVideos) {
|
||||||
|
this.endScreen.setRelatedVideos(relatedVideos);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.player.addChild(this.endScreen);
|
||||||
|
this.endScreen.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupOverlays() {
|
||||||
|
// Clean up any existing overlays
|
||||||
|
if (this.endScreen) {
|
||||||
|
this.player.removeChild(this.endScreen);
|
||||||
|
this.endScreen = null;
|
||||||
|
}
|
||||||
|
if (this.autoplayCountdown) {
|
||||||
|
this.player.removeChild(this.autoplayCountdown);
|
||||||
|
this.autoplayCountdown = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
this.cleanupOverlays();
|
||||||
|
}
|
||||||
|
}
|
||||||
183
frontend-tools/video-js/src/utils/KeyboardHandler.js
Normal file
183
frontend-tools/video-js/src/utils/KeyboardHandler.js
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
/**
|
||||||
|
* KeyboardHandler - Utility for handling video player keyboard controls
|
||||||
|
*
|
||||||
|
* Provides comprehensive keyboard event handling for video players including:
|
||||||
|
* - Space bar for play/pause
|
||||||
|
* - Arrow keys for seeking
|
||||||
|
* - Input field detection to avoid conflicts
|
||||||
|
*/
|
||||||
|
|
||||||
|
class KeyboardHandler {
|
||||||
|
constructor(playerRef, customComponents = null, options = {}) {
|
||||||
|
this.playerRef = playerRef;
|
||||||
|
this.customComponents = customComponents;
|
||||||
|
this.options = {
|
||||||
|
seekAmount: 5, // Default seek amount in seconds
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
this.eventHandler = null;
|
||||||
|
this.isActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an input element is currently focused
|
||||||
|
* @returns {boolean} True if an input element has focus
|
||||||
|
*/
|
||||||
|
isInputFocused() {
|
||||||
|
const activeElement = document.activeElement;
|
||||||
|
return (
|
||||||
|
activeElement &&
|
||||||
|
(activeElement.tagName === 'INPUT' ||
|
||||||
|
activeElement.tagName === 'TEXTAREA' ||
|
||||||
|
activeElement.contentEditable === 'true')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle space key for play/pause functionality
|
||||||
|
* @param {KeyboardEvent} event - The keyboard event
|
||||||
|
*/
|
||||||
|
handleSpaceKey(event) {
|
||||||
|
if (event.code === 'Space' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
if (this.playerRef.current) {
|
||||||
|
if (this.playerRef.current.paused()) {
|
||||||
|
this.playerRef.current.play();
|
||||||
|
} else {
|
||||||
|
this.playerRef.current.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle arrow keys for seeking functionality
|
||||||
|
* @param {KeyboardEvent} event - The keyboard event
|
||||||
|
*/
|
||||||
|
handleArrowKeys(event) {
|
||||||
|
const { seekAmount } = this.options;
|
||||||
|
|
||||||
|
if (event.key === 'ArrowRight' || event.keyCode === 39) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.seekForward(seekAmount);
|
||||||
|
return true;
|
||||||
|
} else if (event.key === 'ArrowLeft' || event.keyCode === 37) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.seekBackward(seekAmount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek forward by specified amount
|
||||||
|
* @param {number} amount - Seconds to seek forward
|
||||||
|
*/
|
||||||
|
seekForward(amount) {
|
||||||
|
if (!this.playerRef.current) return;
|
||||||
|
|
||||||
|
const currentTime = this.playerRef.current.currentTime();
|
||||||
|
const duration = this.playerRef.current.duration();
|
||||||
|
const newTime = Math.min(currentTime + amount, duration);
|
||||||
|
|
||||||
|
this.playerRef.current.currentTime(newTime);
|
||||||
|
|
||||||
|
// Show seek indicator if available
|
||||||
|
if (this.customComponents?.current?.seekIndicator) {
|
||||||
|
this.customComponents.current.seekIndicator.show('forward', amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek backward by specified amount
|
||||||
|
* @param {number} amount - Seconds to seek backward
|
||||||
|
*/
|
||||||
|
seekBackward(amount) {
|
||||||
|
if (!this.playerRef.current) return;
|
||||||
|
|
||||||
|
const currentTime = this.playerRef.current.currentTime();
|
||||||
|
const newTime = Math.max(currentTime - amount, 0);
|
||||||
|
|
||||||
|
this.playerRef.current.currentTime(newTime);
|
||||||
|
|
||||||
|
// Show seek indicator if available
|
||||||
|
if (this.customComponents?.current?.seekIndicator) {
|
||||||
|
this.customComponents.current.seekIndicator.show('backward', amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main keyboard event handler
|
||||||
|
* @param {KeyboardEvent} event - The keyboard event
|
||||||
|
*/
|
||||||
|
handleKeyboardEvent = (event) => {
|
||||||
|
// Only handle if no input elements are focused
|
||||||
|
if (this.isInputFocused()) {
|
||||||
|
return; // Don't interfere with input fields
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle space key for play/pause
|
||||||
|
if (this.handleSpaceKey(event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle arrow keys for seeking
|
||||||
|
if (this.handleArrowKeys(event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize keyboard event handling
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
if (this.isActive) {
|
||||||
|
console.warn('KeyboardHandler is already active');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add keyboard event listener to the document
|
||||||
|
document.addEventListener('keydown', this.handleKeyboardEvent);
|
||||||
|
this.isActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up keyboard event handling
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
if (!this.isActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.removeEventListener('keydown', this.handleKeyboardEvent);
|
||||||
|
this.isActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update options
|
||||||
|
* @param {Object} newOptions - New options to merge
|
||||||
|
*/
|
||||||
|
updateOptions(newOptions) {
|
||||||
|
this.options = { ...this.options, ...newOptions };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update player reference
|
||||||
|
* @param {Object} newPlayerRef - New player reference
|
||||||
|
*/
|
||||||
|
updatePlayerRef(newPlayerRef) {
|
||||||
|
this.playerRef = newPlayerRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update custom components reference
|
||||||
|
* @param {Object} newCustomComponents - New custom components reference
|
||||||
|
*/
|
||||||
|
updateCustomComponents(newCustomComponents) {
|
||||||
|
this.customComponents = newCustomComponents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KeyboardHandler;
|
||||||
65
frontend-tools/video-js/src/utils/OrientationHandler.js
Normal file
65
frontend-tools/video-js/src/utils/OrientationHandler.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
export class OrientationHandler {
|
||||||
|
constructor(player, isTouchDevice) {
|
||||||
|
this.player = player;
|
||||||
|
this.isTouchDevice = isTouchDevice;
|
||||||
|
this.orientationChangeHandler = null;
|
||||||
|
this.screenOrientationHandler = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupOrientationHandling() {
|
||||||
|
// Only apply to mobile/touch devices
|
||||||
|
if (!this.isTouchDevice) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modern approach using Screen Orientation API
|
||||||
|
if (screen.orientation) {
|
||||||
|
this.screenOrientationHandler = () => {
|
||||||
|
const type = screen.orientation.type;
|
||||||
|
|
||||||
|
if (type.includes('landscape')) {
|
||||||
|
// Device rotated to landscape - enter fullscreen
|
||||||
|
if (!this.player.isFullscreen()) {
|
||||||
|
this.player.requestFullscreen();
|
||||||
|
}
|
||||||
|
} else if (type.includes('portrait')) {
|
||||||
|
// Device rotated to portrait - exit fullscreen
|
||||||
|
if (this.player.isFullscreen()) {
|
||||||
|
this.player.exitFullscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
screen.orientation.addEventListener('change', this.screenOrientationHandler);
|
||||||
|
}
|
||||||
|
// Fallback for older iOS devices
|
||||||
|
else {
|
||||||
|
this.orientationChangeHandler = () => {
|
||||||
|
// window.orientation: 0 = portrait, 90/-90 = landscape
|
||||||
|
const isLandscape = Math.abs(window.orientation) === 90;
|
||||||
|
|
||||||
|
// Small delay to ensure orientation change is complete
|
||||||
|
setTimeout(() => {
|
||||||
|
if (isLandscape && !this.player.isFullscreen()) {
|
||||||
|
this.player.requestFullscreen();
|
||||||
|
} else if (!isLandscape && this.player.isFullscreen()) {
|
||||||
|
this.player.exitFullscreen();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('orientationchange', this.orientationChangeHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
// Remove event listeners
|
||||||
|
if (this.screenOrientationHandler && screen.orientation) {
|
||||||
|
screen.orientation.removeEventListener('change', this.screenOrientationHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.orientationChangeHandler) {
|
||||||
|
window.removeEventListener('orientationchange', this.orientationChangeHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
198
frontend-tools/video-js/src/utils/PlaybackEventHandler.js
Normal file
198
frontend-tools/video-js/src/utils/PlaybackEventHandler.js
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
/**
|
||||||
|
* PlaybackEventHandler - Utility for handling video player playback events
|
||||||
|
*
|
||||||
|
* Provides comprehensive playback event handling for video players including:
|
||||||
|
* - Play event handling with seek indicators and embed player visibility
|
||||||
|
* - Pause event handling with poster management
|
||||||
|
* - Quality change detection to prevent unnecessary indicators
|
||||||
|
*/
|
||||||
|
|
||||||
|
class PlaybackEventHandler {
|
||||||
|
constructor(playerRef, customComponents = null, options = {}) {
|
||||||
|
this.playerRef = playerRef;
|
||||||
|
this.customComponents = customComponents;
|
||||||
|
this.options = {
|
||||||
|
isEmbedPlayer: false,
|
||||||
|
showSeekIndicators: true,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
this.eventHandlers = {};
|
||||||
|
this.isActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle play event
|
||||||
|
* Shows play indicator and manages embed player visibility
|
||||||
|
*/
|
||||||
|
handlePlayEvent = () => {
|
||||||
|
const player = this.playerRef.current;
|
||||||
|
if (!player) return;
|
||||||
|
|
||||||
|
// Only show play indicator if not changing quality and indicators are enabled
|
||||||
|
if (
|
||||||
|
!player.isChangingQuality &&
|
||||||
|
this.options.showSeekIndicators &&
|
||||||
|
this.customComponents?.current?.seekIndicator
|
||||||
|
) {
|
||||||
|
this.customComponents.current.seekIndicator.show('play');
|
||||||
|
}
|
||||||
|
|
||||||
|
// For embed players, ensure video becomes visible when playing
|
||||||
|
if (this.options.isEmbedPlayer) {
|
||||||
|
this.handleEmbedPlayerVisibility('play');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle pause event
|
||||||
|
* Shows pause indicator and manages embed player poster visibility
|
||||||
|
*/
|
||||||
|
handlePauseEvent = () => {
|
||||||
|
const player = this.playerRef.current;
|
||||||
|
if (!player) return;
|
||||||
|
|
||||||
|
// Only show pause indicator if not changing quality and indicators are enabled
|
||||||
|
if (
|
||||||
|
!player.isChangingQuality &&
|
||||||
|
this.options.showSeekIndicators &&
|
||||||
|
this.customComponents?.current?.seekIndicator
|
||||||
|
) {
|
||||||
|
this.customComponents.current.seekIndicator.show('pause');
|
||||||
|
}
|
||||||
|
|
||||||
|
// For embed players, show poster when paused at beginning
|
||||||
|
if (this.options.isEmbedPlayer && player.currentTime() === 0) {
|
||||||
|
this.handleEmbedPlayerVisibility('pause');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle embed player visibility for play/pause states
|
||||||
|
* @param {string} action - 'play' or 'pause'
|
||||||
|
*/
|
||||||
|
handleEmbedPlayerVisibility(action) {
|
||||||
|
const player = this.playerRef.current;
|
||||||
|
if (!player) return;
|
||||||
|
|
||||||
|
const playerEl = player.el();
|
||||||
|
const videoEl = playerEl.querySelector('video');
|
||||||
|
const posterEl = playerEl.querySelector('.vjs-poster');
|
||||||
|
const bigPlayButton = player.getChild('bigPlayButton');
|
||||||
|
|
||||||
|
if (action === 'play') {
|
||||||
|
// Make video visible and hide poster
|
||||||
|
if (videoEl) {
|
||||||
|
videoEl.style.opacity = '1';
|
||||||
|
}
|
||||||
|
if (posterEl) {
|
||||||
|
posterEl.style.opacity = '0';
|
||||||
|
}
|
||||||
|
// Hide big play button when video starts playing
|
||||||
|
if (bigPlayButton) {
|
||||||
|
bigPlayButton.hide();
|
||||||
|
}
|
||||||
|
} else if (action === 'pause') {
|
||||||
|
// Hide video and show poster
|
||||||
|
if (videoEl) {
|
||||||
|
videoEl.style.opacity = '0';
|
||||||
|
}
|
||||||
|
if (posterEl) {
|
||||||
|
posterEl.style.opacity = '1';
|
||||||
|
}
|
||||||
|
// Show big play button when paused at beginning
|
||||||
|
if (bigPlayButton) {
|
||||||
|
bigPlayButton.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize playback event handling
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
if (this.isActive) {
|
||||||
|
console.warn('PlaybackEventHandler is already active');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const player = this.playerRef.current;
|
||||||
|
if (!player) {
|
||||||
|
console.error('Player reference is not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
player.on('play', this.handlePlayEvent);
|
||||||
|
player.on('pause', this.handlePauseEvent);
|
||||||
|
|
||||||
|
// Store event handlers for cleanup
|
||||||
|
this.eventHandlers = {
|
||||||
|
play: this.handlePlayEvent,
|
||||||
|
pause: this.handlePauseEvent,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.isActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up playback event handling
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
if (!this.isActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const player = this.playerRef.current;
|
||||||
|
if (player && this.eventHandlers) {
|
||||||
|
// Remove event listeners
|
||||||
|
Object.entries(this.eventHandlers).forEach(([event, handler]) => {
|
||||||
|
player.off(event, handler);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventHandlers = {};
|
||||||
|
this.isActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update options
|
||||||
|
* @param {Object} newOptions - New options to merge
|
||||||
|
*/
|
||||||
|
updateOptions(newOptions) {
|
||||||
|
this.options = { ...this.options, ...newOptions };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update player reference
|
||||||
|
* @param {Object} newPlayerRef - New player reference
|
||||||
|
*/
|
||||||
|
updatePlayerRef(newPlayerRef) {
|
||||||
|
this.playerRef = newPlayerRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update custom components reference
|
||||||
|
* @param {Object} newCustomComponents - New custom components reference
|
||||||
|
*/
|
||||||
|
updateCustomComponents(newCustomComponents) {
|
||||||
|
this.customComponents = newCustomComponents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable seek indicators
|
||||||
|
* @param {boolean} enabled - Whether to show seek indicators
|
||||||
|
*/
|
||||||
|
setSeekIndicatorsEnabled(enabled) {
|
||||||
|
this.options.showSeekIndicators = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set embed player mode
|
||||||
|
* @param {boolean} isEmbed - Whether this is an embed player
|
||||||
|
*/
|
||||||
|
setEmbedPlayerMode(isEmbed) {
|
||||||
|
this.options.isEmbedPlayer = isEmbed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PlaybackEventHandler;
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user