mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-10-06 22:32:41 +07:00
feat: Autoplay functionality
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
/* Autoplay Countdown Overlay Styles */
|
||||
.vjs-autoplay-countdown-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: calc(100% - 46px); /* Account for control bar */
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
backdrop-filter: blur(4px);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.vjs-autoplay-countdown-overlay.autoplay-countdown-show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.autoplay-countdown-content {
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.autoplay-countdown-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.autoplay-countdown-header h3 {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.countdown-timer {
|
||||
color: #ff4444;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
animation: countdownPulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes countdownPulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.autoplay-countdown-video-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.next-video-thumbnail {
|
||||
flex-shrink: 0;
|
||||
width: 80px;
|
||||
height: 45px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.next-video-thumbnail img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.next-video-details {
|
||||
flex-grow: 1;
|
||||
min-width: 0; /* Allow text truncation */
|
||||
}
|
||||
|
||||
.next-video-title {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 4px 0;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.next-video-author {
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
margin: 0 0 2px 0;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.next-video-duration {
|
||||
color: #ccc;
|
||||
font-size: 11px;
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.autoplay-countdown-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.autoplay-play-button,
|
||||
.autoplay-cancel-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 10px 18px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.autoplay-play-button {
|
||||
background: #ff0000;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.autoplay-play-button:hover {
|
||||
background: #e60000;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.autoplay-play-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.autoplay-cancel-button {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.autoplay-cancel-button:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.autoplay-cancel-button:active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.autoplay-play-button svg,
|
||||
.autoplay-cancel-button svg {
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Autoplay Toggle Button Styles */
|
||||
.vjs-autoplay-toggle {
|
||||
width: 3em !important;
|
||||
height: 3em !important;
|
||||
flex: none;
|
||||
padding: 0 !important;
|
||||
margin: 0 4px !important;
|
||||
line-height: 3em !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.vjs-autoplay-toggle .vjs-autoplay-icon {
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.vjs-autoplay-toggle .vjs-autoplay-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 480px) {
|
||||
.autoplay-countdown-content {
|
||||
padding: 20px;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.autoplay-countdown-header h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.countdown-timer {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.autoplay-countdown-video-info {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.next-video-thumbnail {
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.next-video-title {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.autoplay-countdown-actions {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.autoplay-play-button,
|
||||
.autoplay-cancel-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
import videojs from 'video.js';
|
||||
import './AutoplayCountdownOverlay.css';
|
||||
|
||||
// Get the Component base class from Video.js
|
||||
const Component = videojs.getComponent('Component');
|
||||
|
||||
class AutoplayCountdownOverlay extends Component {
|
||||
constructor(player, options) {
|
||||
super(player, options);
|
||||
|
||||
this.nextVideoData = options.nextVideoData || null;
|
||||
this.countdownSeconds = options.countdownSeconds || 5;
|
||||
this.onPlayNext = options.onPlayNext || (() => {});
|
||||
this.onCancel = options.onCancel || (() => {});
|
||||
|
||||
this.currentCountdown = this.countdownSeconds;
|
||||
this.countdownInterval = null;
|
||||
this.isActive = false;
|
||||
|
||||
// Bind methods
|
||||
this.startCountdown = this.startCountdown.bind(this);
|
||||
this.stopCountdown = this.stopCountdown.bind(this);
|
||||
this.handlePlayNext = this.handlePlayNext.bind(this);
|
||||
this.handleCancel = this.handleCancel.bind(this);
|
||||
this.updateCountdownDisplay = this.updateCountdownDisplay.bind(this);
|
||||
}
|
||||
|
||||
createEl() {
|
||||
const overlay = super.createEl('div', {
|
||||
className: 'vjs-autoplay-countdown-overlay',
|
||||
});
|
||||
|
||||
// Get next video title or fallback
|
||||
const nextVideoTitle = this.nextVideoData?.title || 'Next Video';
|
||||
const nextVideoThumbnail = this.nextVideoData?.thumbnail || '';
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div class="autoplay-countdown-content">
|
||||
<div class="autoplay-countdown-header">
|
||||
<h3>Up next in <span class="countdown-timer">${this.countdownSeconds}</span></h3>
|
||||
</div>
|
||||
|
||||
<div class="autoplay-countdown-video-info">
|
||||
${
|
||||
nextVideoThumbnail
|
||||
? `<div class="next-video-thumbnail">
|
||||
<img src="${nextVideoThumbnail}" alt="${nextVideoTitle}" />
|
||||
</div>`
|
||||
: ''
|
||||
}
|
||||
<div class="next-video-details">
|
||||
<h4 class="next-video-title">${nextVideoTitle}</h4>
|
||||
${this.nextVideoData?.author ? `<p class="next-video-author">${this.nextVideoData.author}</p>` : ''}
|
||||
${this.nextVideoData?.duration ? `<p class="next-video-duration">${this.formatDuration(this.nextVideoData.duration)}</p>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="autoplay-countdown-actions">
|
||||
<button class="autoplay-play-button" type="button">
|
||||
<svg viewBox="0 0 24 24" width="1.2em" height="1.2em" fill="currentColor">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
</svg>
|
||||
Play Now
|
||||
</button>
|
||||
<button class="autoplay-cancel-button" type="button">
|
||||
<svg viewBox="0 0 24 24" width="1.2em" height="1.2em" fill="currentColor">
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
|
||||
</svg>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add event listeners with explicit binding
|
||||
const playButton = overlay.querySelector('.autoplay-play-button');
|
||||
const cancelButton = overlay.querySelector('.autoplay-cancel-button');
|
||||
|
||||
if (playButton) {
|
||||
playButton.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.handlePlayNext();
|
||||
});
|
||||
}
|
||||
|
||||
if (cancelButton) {
|
||||
cancelButton.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.handleCancel();
|
||||
});
|
||||
}
|
||||
|
||||
// Initially hide the overlay
|
||||
overlay.style.display = 'none';
|
||||
|
||||
return overlay;
|
||||
}
|
||||
|
||||
startCountdown() {
|
||||
this.isActive = true;
|
||||
this.currentCountdown = this.countdownSeconds;
|
||||
this.show();
|
||||
this.updateCountdownDisplay();
|
||||
|
||||
// Start countdown interval
|
||||
this.countdownInterval = setInterval(() => {
|
||||
this.currentCountdown--;
|
||||
this.updateCountdownDisplay();
|
||||
|
||||
if (this.currentCountdown <= 0) {
|
||||
this.stopCountdown();
|
||||
// Auto-play next video when countdown reaches 0
|
||||
this.handlePlayNext();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
console.log('Autoplay countdown started:', this.countdownSeconds, 'seconds');
|
||||
}
|
||||
|
||||
stopCountdown() {
|
||||
this.isActive = false;
|
||||
if (this.countdownInterval) {
|
||||
clearInterval(this.countdownInterval);
|
||||
this.countdownInterval = null;
|
||||
}
|
||||
this.hide();
|
||||
console.log('Autoplay countdown stopped');
|
||||
}
|
||||
|
||||
updateCountdownDisplay() {
|
||||
const timerElement = this.el().querySelector('.countdown-timer');
|
||||
if (timerElement) {
|
||||
timerElement.textContent = this.currentCountdown;
|
||||
}
|
||||
}
|
||||
|
||||
handlePlayNext() {
|
||||
console.log('Autoplay: Playing next video immediately');
|
||||
try {
|
||||
this.stopCountdown();
|
||||
this.onPlayNext();
|
||||
} catch (error) {
|
||||
console.error('Error in handlePlayNext:', error);
|
||||
}
|
||||
}
|
||||
|
||||
handleCancel() {
|
||||
console.log('Autoplay: Cancelled by user');
|
||||
try {
|
||||
this.stopCountdown();
|
||||
this.onCancel();
|
||||
} catch (error) {
|
||||
console.error('Error in handleCancel:', error);
|
||||
}
|
||||
}
|
||||
|
||||
show() {
|
||||
if (this.el()) {
|
||||
this.el().style.display = 'flex';
|
||||
// Add animation class for smooth entrance
|
||||
this.el().classList.add('autoplay-countdown-show');
|
||||
}
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (this.el()) {
|
||||
this.el().style.display = 'none';
|
||||
this.el().classList.remove('autoplay-countdown-show');
|
||||
}
|
||||
}
|
||||
|
||||
formatDuration(seconds) {
|
||||
if (!seconds || seconds === 0) return '';
|
||||
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = seconds % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
} else {
|
||||
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Update next video data
|
||||
updateNextVideoData(nextVideoData) {
|
||||
this.nextVideoData = nextVideoData;
|
||||
|
||||
// Re-render the content if the overlay exists
|
||||
if (this.el()) {
|
||||
const nextVideoTitle = this.nextVideoData?.title || 'Next Video';
|
||||
const nextVideoThumbnail = this.nextVideoData?.thumbnail || '';
|
||||
|
||||
const videoInfoElement = this.el().querySelector('.autoplay-countdown-video-info');
|
||||
if (videoInfoElement) {
|
||||
videoInfoElement.innerHTML = `
|
||||
${
|
||||
nextVideoThumbnail
|
||||
? `<div class="next-video-thumbnail">
|
||||
<img src="${nextVideoThumbnail}" alt="${nextVideoTitle}" />
|
||||
</div>`
|
||||
: ''
|
||||
}
|
||||
<div class="next-video-details">
|
||||
<h4 class="next-video-title">${nextVideoTitle}</h4>
|
||||
${this.nextVideoData?.author ? `<p class="next-video-author">${this.nextVideoData.author}</p>` : ''}
|
||||
${this.nextVideoData?.duration ? `<p class="next-video-duration">${this.formatDuration(this.nextVideoData.duration)}</p>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup method
|
||||
dispose() {
|
||||
this.stopCountdown();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Register the component
|
||||
videojs.registerComponent('AutoplayCountdownOverlay', AutoplayCountdownOverlay);
|
||||
|
||||
export default AutoplayCountdownOverlay;
|
||||
Reference in New Issue
Block a user