mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-10-06 22:32:41 +07:00
fix: Video.js (chaptersData conditions, separate sprite preview, add close icon in modal settings)
This commit is contained in:
@@ -539,6 +539,12 @@ white-space: nowrap; text-overflow: ellipsis; height: 28px; overflow: hidden; di
|
||||
.chapter-close button{ background:transparent; color:#fff; border: 0; width: 40px; height: 40px; padding: 0; display: flex;
|
||||
align-items: center; justify-content: center; border-radius:8px;}
|
||||
.chapter-close button:hover{ background:rgba(255,255,255,0.1);}
|
||||
|
||||
.settings-header{ display:flex; align-items:center; justify-content:space-between; position:relative;}
|
||||
.settings-close-btn{ background:transparent; color:#fff; border: 0; width: 32px; height: 32px; padding: 0; display: flex;
|
||||
align-items: center; justify-content: center; border-radius:6px; cursor:pointer;}
|
||||
.settings-close-btn:hover{ background:rgba(255,255,255,0.1);}
|
||||
|
||||
.playlist-action-menu{ display:none; justify-content:space-between; gap:10px;}
|
||||
.playlist-action-menu button{ background:transparent; border: 0; width: 40px; height: 40px; padding: 0; display: flex;
|
||||
align-items: center; justify-content: center; align-items:center; border-radius:100px; }
|
||||
@@ -596,11 +602,16 @@ overflow:visible !important; clip: initial !important;}
|
||||
.vjs-chapter-floating-tooltip{ text-align:center; width:160px !important; max-width:100% !important ; height:auto;}
|
||||
.chapter-image-sprite{width: 166px !important; max-width:100% !important; height: 96px; margin:0 auto 10px;
|
||||
border-radius: 6px; border: 3px solid #FFF; }
|
||||
.vjs-chapter-floating-tooltip .chapter-title{ font-size:24px; margin:0 0 10px; font-weight:700; text-overflow:ellipsis; white-space:nowrap;
|
||||
height:30px; line-height:30px; overflow:hidden;}
|
||||
.vjs-chapter-floating-tooltip .chapter-title{ font-size:16px; margin:0 0 10px; font-weight:700; word-break: break-all; line-height:20px;}
|
||||
.vjs-chapter-floating-tooltip .position-info,
|
||||
.vjs-chapter-floating-tooltip .chapter-info{ font-size:15px; display:inline-block; margin:0 0 2px; line-height:normal; vertical-align:top; line-height:20px;}
|
||||
|
||||
/* Sprite Preview Tooltip Styles - Match chapter styling */
|
||||
.vjs-sprite-preview-tooltip{ text-align:center; width:172px !important; max-width:100% !important ; height:auto;}
|
||||
|
||||
.vjs-sprite-preview-tooltip .sprite-image-preview{ width: 166px !important; max-width:100% !important; height: 96px; margin:0 auto;
|
||||
border-radius: 6px; border: 3px solid #FFF; }
|
||||
|
||||
|
||||
@media (pointer: coarse) {
|
||||
.video-js .vjs-volume-panel div.vjs-volume-control {width: auto; opacity: 1;}
|
||||
|
||||
@@ -155,7 +155,15 @@ class CustomSettingsMenu extends Component {
|
||||
(currentQuality ? String(currentQuality) : "Auto");
|
||||
|
||||
// Settings menu content - split into separate variables for maintainability
|
||||
const settingsHeader = `<div class="settings-header">Settings</div>`;
|
||||
const settingsHeader = `
|
||||
<div class="settings-header">
|
||||
<span>Settings</span>
|
||||
<button class="settings-close-btn" aria-label="Close settings">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.7096 12L20.8596 20.15L20.1496 20.86L11.9996 12.71L3.84965 20.86L3.13965 20.15L11.2896 12L3.14965 3.85001L3.85965 3.14001L11.9996 11.29L20.1496 3.14001L20.8596 3.85001L12.7096 12Z" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
const playbackSpeedSection = `
|
||||
<div class="settings-item" data-setting="playback-speed">
|
||||
@@ -527,6 +535,29 @@ class CustomSettingsMenu extends Component {
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Close button functionality
|
||||
const closeButton = this.settingsOverlay.querySelector('.settings-close-btn');
|
||||
if (closeButton) {
|
||||
const closeFunction = (e) => {
|
||||
e.stopPropagation();
|
||||
this.settingsOverlay.classList.remove("show");
|
||||
this.settingsOverlay.style.display = "none";
|
||||
this.speedSubmenu.style.display = "none";
|
||||
if (this.qualitySubmenu) this.qualitySubmenu.style.display = "none";
|
||||
if (this.subtitlesSubmenu) this.subtitlesSubmenu.style.display = "none";
|
||||
const btnEl = this.settingsButton?.el();
|
||||
if (btnEl) {
|
||||
btnEl.classList.remove("settings-clicked");
|
||||
}
|
||||
};
|
||||
|
||||
closeButton.addEventListener('click', closeFunction);
|
||||
closeButton.addEventListener('touchend', (e) => {
|
||||
e.preventDefault();
|
||||
closeFunction(e);
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
// Settings item clicks
|
||||
this.settingsOverlay.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -218,7 +218,7 @@ class ChapterMarkers extends Component {
|
||||
// Update text content without rebuilding DOM
|
||||
this.chapterTitle.textContent = currentChapter.text;
|
||||
this.chapterInfo.textContent = `Chapter: ${startTime} - ${endTime}`;
|
||||
this.positionInfo.textContent = `Position: ${timeAtPosition}`;
|
||||
// this.positionInfo.textContent = `Position: ${timeAtPosition}`;
|
||||
|
||||
// Update sprite thumbnail
|
||||
this.updateSpriteThumbnail(currentTime);
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
import videojs from 'video.js';
|
||||
|
||||
const Component = videojs.getComponent('Component');
|
||||
|
||||
// Sprite Preview Component for seekbar hover thumbnails (used when no chapters exist)
|
||||
class SpritePreview extends Component {
|
||||
constructor(player, options) {
|
||||
super(player, options);
|
||||
this.tooltip = null;
|
||||
this.isHovering = false;
|
||||
this.previewSprite = options.previewSprite || null;
|
||||
}
|
||||
|
||||
createEl() {
|
||||
const el = super.createEl('div', {
|
||||
className: 'vjs-sprite-preview-track',
|
||||
});
|
||||
|
||||
// Initialize tooltip as null - will be created when needed
|
||||
this.tooltip = null;
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
setupProgressBarHover() {
|
||||
const progressControl = this.player().getChild('controlBar').getChild('progressControl');
|
||||
if (!progressControl) return;
|
||||
|
||||
const seekBar = progressControl.getChild('seekBar');
|
||||
if (!seekBar) return;
|
||||
|
||||
const seekBarEl = seekBar.el();
|
||||
|
||||
// Only setup if we have sprite data
|
||||
if (!this.previewSprite || !this.previewSprite.url) {
|
||||
console.log('No sprite data available for preview:', this.previewSprite);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure tooltip is properly created and add to seekBar if not already added
|
||||
if (!this.tooltip || !this.tooltip.nodeType) {
|
||||
// Create tooltip if it's not a proper DOM node
|
||||
this.tooltip = videojs.dom.createEl('div', {
|
||||
className: 'vjs-sprite-preview-tooltip',
|
||||
});
|
||||
|
||||
// Style the floating tooltip
|
||||
Object.assign(this.tooltip.style, {
|
||||
position: 'absolute',
|
||||
zIndex: '1000',
|
||||
bottom: '45px',
|
||||
transform: 'translateX(-50%)',
|
||||
display: 'none',
|
||||
minWidth: '172px', // Accommodate 166px image + 3px border on each side
|
||||
maxWidth: '172px',
|
||||
width: '172px',
|
||||
});
|
||||
|
||||
// Create stable DOM structure
|
||||
this.spriteImage = videojs.dom.createEl('div', {
|
||||
className: 'sprite-image-preview',
|
||||
});
|
||||
Object.assign(this.spriteImage.style, {
|
||||
display: 'block',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
// Append sprite image to tooltip (no time info)
|
||||
this.tooltip.appendChild(this.spriteImage);
|
||||
}
|
||||
|
||||
// Add tooltip to seekBar if not already added
|
||||
if (!seekBarEl.querySelector('.vjs-sprite-preview-tooltip')) {
|
||||
try {
|
||||
seekBarEl.appendChild(this.tooltip);
|
||||
} catch (error) {
|
||||
console.warn('Could not append sprite preview tooltip:', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the progress control element for larger hover area
|
||||
const progressControlEl = progressControl.el();
|
||||
|
||||
// Remove existing listeners to prevent duplicates
|
||||
progressControlEl.removeEventListener('mouseenter', this.handleMouseEnter);
|
||||
progressControlEl.removeEventListener('mouseleave', this.handleMouseLeave);
|
||||
progressControlEl.removeEventListener('mousemove', this.handleMouseMove);
|
||||
|
||||
// Bind methods to preserve context
|
||||
this.handleMouseEnter = () => {
|
||||
this.isHovering = true;
|
||||
this.tooltip.style.display = 'block';
|
||||
};
|
||||
|
||||
this.handleMouseLeave = () => {
|
||||
this.isHovering = false;
|
||||
this.tooltip.style.display = 'none';
|
||||
};
|
||||
|
||||
this.handleMouseMove = (e) => {
|
||||
if (!this.isHovering) return;
|
||||
this.updateSpriteTooltip(e, seekBarEl, progressControlEl);
|
||||
};
|
||||
|
||||
// Add event listeners to the entire progress control area
|
||||
progressControlEl.addEventListener('mouseenter', this.handleMouseEnter);
|
||||
progressControlEl.addEventListener('mouseleave', this.handleMouseLeave);
|
||||
progressControlEl.addEventListener('mousemove', this.handleMouseMove);
|
||||
}
|
||||
|
||||
updateSpriteTooltip(event, seekBarEl, progressControlEl) {
|
||||
if (!this.tooltip || !this.isHovering) return;
|
||||
|
||||
const duration = this.player().duration();
|
||||
if (!duration) return;
|
||||
|
||||
// Calculate time position based on mouse position relative to seekBar
|
||||
const seekBarRect = seekBarEl.getBoundingClientRect();
|
||||
const progressControlRect = progressControlEl.getBoundingClientRect();
|
||||
|
||||
// Use seekBar for horizontal calculation but allow vertical tolerance
|
||||
const offsetX = event.clientX - seekBarRect.left;
|
||||
const percentage = Math.max(0, Math.min(1, offsetX / seekBarRect.width));
|
||||
const currentTime = percentage * duration;
|
||||
|
||||
// Position tooltip relative to progress control area
|
||||
const tooltipOffsetX = event.clientX - progressControlRect.left;
|
||||
|
||||
// Update sprite thumbnail
|
||||
this.updateSpriteThumbnail(currentTime);
|
||||
|
||||
// Position tooltip with smart boundary detection
|
||||
// Force tooltip to be visible momentarily to get accurate dimensions
|
||||
this.tooltip.style.visibility = 'hidden';
|
||||
this.tooltip.style.display = 'block';
|
||||
|
||||
const tooltipWidth = this.tooltip.offsetWidth || 172; // Fallback width matches our fixed width
|
||||
const progressControlWidth = progressControlRect.width;
|
||||
const halfTooltipWidth = tooltipWidth / 2;
|
||||
|
||||
// Calculate ideal position (where mouse is)
|
||||
let idealLeft = tooltipOffsetX;
|
||||
|
||||
// Check and adjust boundaries
|
||||
if (idealLeft - halfTooltipWidth < 0) {
|
||||
// Too far left - align to left edge with small margin
|
||||
idealLeft = halfTooltipWidth + 5;
|
||||
} else if (idealLeft + halfTooltipWidth > progressControlWidth) {
|
||||
// Too far right - align to right edge with small margin
|
||||
idealLeft = progressControlWidth - halfTooltipWidth - 5;
|
||||
}
|
||||
|
||||
// Apply position and make visible
|
||||
this.tooltip.style.left = `${idealLeft}px`;
|
||||
this.tooltip.style.visibility = 'visible';
|
||||
this.tooltip.style.display = 'block';
|
||||
}
|
||||
|
||||
updateSpriteThumbnail(currentTime) {
|
||||
if (!this.previewSprite || !this.previewSprite.url) {
|
||||
// Hide image if no sprite data available
|
||||
this.spriteImage.style.display = 'none';
|
||||
console.log('No sprite data available:', this.previewSprite);
|
||||
return;
|
||||
}
|
||||
|
||||
const { url, frame } = this.previewSprite;
|
||||
const { width, height } = frame;
|
||||
|
||||
// Calculate which frame to show based on current time
|
||||
// Use sprite interval from frame data, fallback to 10 seconds
|
||||
const frameInterval = frame.seconds || 10;
|
||||
|
||||
// Try to detect total frames based on video duration vs frame interval
|
||||
const videoDuration = this.player().duration() || 45; // fallback duration
|
||||
const calculatedMaxFrames = Math.ceil(videoDuration / frameInterval);
|
||||
const maxFrames = Math.min(calculatedMaxFrames, 6); // Cap at 6 frames to be safe
|
||||
|
||||
let frameIndex = Math.floor(currentTime / frameInterval);
|
||||
|
||||
// Clamp frameIndex to available frames to prevent showing empty areas
|
||||
frameIndex = Math.min(frameIndex, maxFrames - 1);
|
||||
|
||||
// Based on the sprite image, it appears to have frames arranged vertically
|
||||
// Let's try a vertical layout first (1 column, multiple rows)
|
||||
const frameRow = frameIndex; // Each frame is on its own row
|
||||
const frameCol = 0; // Always first (and only) column
|
||||
|
||||
// Calculate background position (negative values to shift the sprite)
|
||||
const xPos = -(frameCol * width);
|
||||
const yPos = -(frameRow * height);
|
||||
|
||||
console.log(
|
||||
`Sprite Preview - Time: ${currentTime}s, Duration: ${this.player().duration()}s, Interval: ${frameInterval}s, Frame: ${frameIndex}/${maxFrames - 1}, Row: ${frameRow}, Col: ${frameCol}, Pos: ${xPos}px ${yPos}px, URL: ${url}`
|
||||
);
|
||||
|
||||
// Apply sprite background
|
||||
this.spriteImage.style.backgroundImage = `url("${url}")`;
|
||||
this.spriteImage.style.backgroundPosition = `${xPos}px ${yPos}px`;
|
||||
this.spriteImage.style.backgroundSize = 'auto';
|
||||
this.spriteImage.style.backgroundRepeat = 'no-repeat';
|
||||
// Use CSS-defined dimensions (166x96) to match chapter styling
|
||||
this.spriteImage.style.width = '166px';
|
||||
this.spriteImage.style.height = '96px';
|
||||
|
||||
// Ensure the image is visible
|
||||
this.spriteImage.style.display = 'block';
|
||||
|
||||
// Fallback: if we're beyond frame 3 (30s+), try showing frame 2 instead (20-30s frame)
|
||||
if (frameIndex >= 3 && currentTime > 30) {
|
||||
const fallbackYPos = -(2 * height); // Frame 2 (20-30s range)
|
||||
this.spriteImage.style.backgroundPosition = `${xPos}px ${fallbackYPos}px`;
|
||||
console.log(`Fallback: Using frame 2 instead of frame ${frameIndex} for time ${currentTime}s`);
|
||||
}
|
||||
}
|
||||
|
||||
formatTime(seconds) {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
// Clean up event listeners
|
||||
const progressControl = this.player().getChild('controlBar')?.getChild('progressControl');
|
||||
if (progressControl) {
|
||||
const progressControlEl = progressControl.el();
|
||||
progressControlEl.removeEventListener('mouseenter', this.handleMouseEnter);
|
||||
progressControlEl.removeEventListener('mouseleave', this.handleMouseLeave);
|
||||
progressControlEl.removeEventListener('mousemove', this.handleMouseMove);
|
||||
}
|
||||
|
||||
// Remove tooltip
|
||||
if (this.tooltip && this.tooltip.parentNode) {
|
||||
this.tooltip.parentNode.removeChild(this.tooltip);
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Register the sprite preview component
|
||||
videojs.registerComponent('SpritePreview', SpritePreview);
|
||||
|
||||
export default SpritePreview;
|
||||
@@ -6,6 +6,7 @@ import 'video.js/dist/video-js.css';
|
||||
import EndScreenOverlay from '../overlays/EndScreenOverlay';
|
||||
import AutoplayCountdownOverlay from '../overlays/AutoplayCountdownOverlay';
|
||||
import ChapterMarkers from '../markers/ChapterMarkers';
|
||||
import SpritePreview from '../markers/SpritePreview';
|
||||
import NextVideoButton from '../controls/NextVideoButton';
|
||||
import AutoplayToggleButton from '../controls/AutoplayToggleButton';
|
||||
import CustomRemainingTime from '../controls/CustomRemainingTime';
|
||||
@@ -634,6 +635,10 @@ function VideoJSPlayer() {
|
||||
|
||||
// Define chapters as JSON object
|
||||
// Note: The sample-chapters.vtt file is no longer needed as chapters are now loaded from this JSON
|
||||
// CONDITIONAL LOGIC:
|
||||
// - When chaptersData has content: Uses original ChapterMarkers with sprite preview
|
||||
// - When chaptersData is empty: Uses separate SpritePreview component
|
||||
// Toggle between these two lines to test both scenarios:
|
||||
const chaptersData = mediaData?.data?.chapter_data && mediaData?.data?.chapter_data.length > 0 ? mediaData?.data?.chapter_data : isDevelopment ? [
|
||||
{ startTime: 0, endTime: 4, text: 'Introduction' },
|
||||
{ startTime: 5, endTime: 10, text: 'Overview of Marine Life' },
|
||||
@@ -658,7 +663,7 @@ function VideoJSPlayer() {
|
||||
{ startTime: 1440, endTime: 1520, text: 'Commercial Aquaculture' },
|
||||
{ startTime: 1520, endTime: 1600, text: 'Ocean Exploration Technology' },
|
||||
] : [];
|
||||
// const chaptersData = [];
|
||||
// const chaptersData = []; // NO CHAPTERS (uses separate SpritePreview)
|
||||
|
||||
// Get video data from mediaData
|
||||
const currentVideo = useMemo(
|
||||
@@ -1531,14 +1536,40 @@ function VideoJSPlayer() {
|
||||
setupClickableMenus();
|
||||
}, 1500);
|
||||
|
||||
// BEGIN: Add chapter markers to progress control
|
||||
// BEGIN: Add chapter markers and sprite preview to progress control
|
||||
if (progressControl && seekBar) {
|
||||
const chapterMarkers = new ChapterMarkers(playerRef.current, {
|
||||
previewSprite: mediaData.previewSprite,
|
||||
});
|
||||
seekBar.addChild(chapterMarkers);
|
||||
console.log('Setting up sprite preview and chapter markers...');
|
||||
console.log('mediaData.previewSprite:', mediaData.previewSprite);
|
||||
console.log('chaptersData:', chaptersData);
|
||||
|
||||
// Check if we have chapters
|
||||
const hasChapters = chaptersData && chaptersData.length > 0;
|
||||
|
||||
if (hasChapters) {
|
||||
// Use original ChapterMarkers with sprite functionality when chapters exist
|
||||
console.log('✓ Adding ChapterMarkers component with sprite functionality (chapters exist)');
|
||||
const chapterMarkers = new ChapterMarkers(playerRef.current, {
|
||||
previewSprite: mediaData.previewSprite,
|
||||
});
|
||||
seekBar.addChild(chapterMarkers);
|
||||
} else if (mediaData.previewSprite) {
|
||||
// Use separate SpritePreview component only when no chapters but sprite data exists
|
||||
console.log('✓ Adding SpritePreview component (no chapters, but sprite data available)');
|
||||
const spritePreview = new SpritePreview(playerRef.current, {
|
||||
previewSprite: mediaData.previewSprite,
|
||||
});
|
||||
seekBar.addChild(spritePreview);
|
||||
|
||||
// Setup sprite preview hover functionality
|
||||
setTimeout(() => {
|
||||
console.log('✓ Setting up sprite preview hover functionality');
|
||||
spritePreview.setupProgressBarHover();
|
||||
}, 100);
|
||||
} else {
|
||||
console.log('✗ No chapters and no sprite data available');
|
||||
}
|
||||
}
|
||||
// END: Add chapter markers to progress control
|
||||
// END: Add chapter markers and sprite preview to progress control
|
||||
|
||||
// BEGIN: Simple button layout fix - use CSS approach
|
||||
setTimeout(() => {
|
||||
|
||||
Reference in New Issue
Block a user