Add Overlay Video Player - Portable popup video player system

NEW FEATURE: Overlay Video Player
- Created OverlayVideoPlayer class extending BaseVideoPlayer for maximum reusability
- Implements popup/modal video player with professional UI design
- Random video selection from unified video library with fallback mechanisms
- Full video controls inherited from BaseVideoPlayer (play/pause, seek, volume, fullscreen)

 KEY FEATURES:
- Modal overlay with backdrop blur and smooth animations
- Dismissible via Escape key, close button, or backdrop click
- Random video button () for quick video switching
- Auto-displays video metadata (name, size, duration) in header and controls
- Responsive design with mobile optimization
- Professional glassmorphism styling with dark theme

 TECHNICAL IMPLEMENTATION:
- Extends BaseVideoPlayer for consistent behavior across all video players
- Uses unique DOM IDs to avoid selector conflicts
- Temporary DOM manipulation for proper BaseVideoPlayer initialization
- CSS animations and transitions for smooth user experience
- Integration with existing video management and storage systems

 TESTING:
- Accessible via 'Test Overlay Video' button in Video Management screen
- Successfully loads random videos from linked directories (E:\Videos\M1K1)
- Fully functional video controls and dismissal mechanisms

This demonstrates the portability of our BaseVideoPlayer architecture
and provides a foundation for future popup/overlay video implementations.
This commit is contained in:
dilgenfritz 2025-10-31 15:18:44 -05:00
parent 220f347269
commit 0067af93ef
3 changed files with 735 additions and 3 deletions

View File

@ -7,6 +7,7 @@
<title>Gooner Training Academy - Master Your Dedication</title>
<link rel="stylesheet" href="src/styles/styles.css">
<link rel="stylesheet" href="src/styles/base-video-player.css">
<link rel="stylesheet" href="src/styles/overlay-video-player.css">
<link href="https://fonts.googleapis.com/css2?family=Audiowide&display=swap" rel="stylesheet">
<script src="https://unpkg.com/jszip@3.10.1/dist/jszip.min.js" crossorigin="anonymous"></script>
<script>
@ -1657,6 +1658,7 @@
<script src="src/features/tts/voiceManager.js"></script>
<script src="src/features/media/baseVideoPlayer.js"></script>
<script src="src/features/media/focusVideoPlayer.js"></script>
<script src="src/features/media/overlayVideoPlayer.js"></script>
<script src="src/features/tasks/interactiveTaskManager.js"></script>
<script src="src/features/video/videoPlayerManager.js"></script>
<script src="src/utils/desktop-file-manager.js"></script>
@ -2854,10 +2856,31 @@
}
}
function testVideoPlayback(type) {
async function testVideoPlayback(type) {
console.log(`Testing ${type} video playback`);
if (window.game && window.game.showNotification) {
window.game.showNotification(`${type} video test - feature coming soon!`, 'info');
if (type === 'overlay') {
try {
// Show overlay video player with random video
const overlayPlayer = await OverlayVideoPlayer.showOverlay({
showQuality: false,
showSpeed: false,
minimal: false
});
if (window.game && window.game.showNotification) {
window.game.showNotification('🎬 Overlay video player opened!', 'success');
}
} catch (error) {
console.error('Error showing overlay video player:', error);
if (window.game && window.game.showNotification) {
window.game.showNotification('Error opening overlay video player', 'error');
}
}
} else {
if (window.game && window.game.showNotification) {
window.game.showNotification(`${type} video test - feature coming soon!`, 'info');
}
}
}

View File

@ -0,0 +1,318 @@
/**
* Overlay Video Player
* A popup overlay video player that extends BaseVideoPlayer
* Used for random video playback in overlay/popup windows
*/
class OverlayVideoPlayer extends BaseVideoPlayer {
constructor(options = {}) {
// Create the overlay container first
const overlayContainer = OverlayVideoPlayer.createOverlayContainer();
// Add it to DOM temporarily so we can query it
document.body.appendChild(overlayContainer);
// Create a unique ID for the video container
const containerId = `overlay-video-container-${Date.now()}`;
const videoContainer = overlayContainer.querySelector('.overlay-video-container');
videoContainer.id = containerId;
// Initialize BaseVideoPlayer with the CSS selector
super(`#${containerId}`, {
showControls: true,
autoHide: true,
showProgress: true,
showVolume: true,
showFullscreen: true,
showQuality: false, // Keep it simple for overlay
showSpeed: false, // Keep it simple for overlay
keyboardShortcuts: true,
minimal: false,
...options
});
this.overlayElement = overlayContainer;
this.isVisible = false;
this.currentVideo = null;
// Remove from DOM initially (will be added back when shown)
document.body.removeChild(overlayContainer);
// Setup overlay-specific events
this.setupOverlayEvents();
console.log('🎬 OverlayVideoPlayer created');
}
static createOverlayContainer() {
const overlay = document.createElement('div');
overlay.className = 'video-overlay-popup';
overlay.innerHTML = `
<div class="overlay-backdrop" id="overlay-backdrop"></div>
<div class="overlay-window">
<div class="overlay-header">
<h3 class="overlay-title">🎬 Video Player</h3>
<button class="overlay-close-btn" id="overlay-close-btn" title="Close (Esc)"></button>
</div>
<div class="overlay-video-container">
<video class="main-video" preload="metadata">
<source id="video-source" src="" type="video/mp4">
Your browser does not support the video tag.
</video>
<!-- Video Overlay Controls (inherited from BaseVideoPlayer) -->
<div class="video-overlay">
<div class="video-title" id="video-title">Loading video...</div>
<div class="video-info" id="video-info"></div>
<!-- Play Button Overlay -->
<div class="play-overlay">
<button class="play-button-large"></button>
</div>
<!-- Loading Spinner -->
<div class="video-loading" style="display: none;">
<div class="loading-spinner"></div>
<p>Loading video...</p>
</div>
</div>
<!-- Custom Video Controls (inherited from BaseVideoPlayer) -->
<div class="video-controls">
<div class="progress-container">
<div class="progress-bar">
<div class="progress-filled"></div>
<div class="progress-thumb"></div>
</div>
<div class="time-display">
<span class="current-time">0:00</span>
<span class="time-separator">/</span>
<span class="total-time">0:00</span>
</div>
</div>
<div class="controls-row">
<div class="controls-left">
<button class="control-btn play-pause-btn"></button>
<div class="volume-control">
<button class="control-btn mute-btn" title="Mute (M)">🔊</button>
<input type="range" class="volume-slider" min="0" max="100" value="70">
<span class="volume-percentage">70%</span>
</div>
</div>
<div class="controls-center">
<span class="video-name"></span>
</div>
<div class="controls-right">
<button class="control-btn random-video-btn" title="Random Video">🎲</button>
<button class="control-btn fullscreen-btn" title="Fullscreen (F)"></button>
</div>
</div>
</div>
</div>
</div>
`;
return overlay;
}
setupOverlayEvents() {
// Close button
const closeBtn = this.overlayElement.querySelector('#overlay-close-btn');
closeBtn.addEventListener('click', () => this.hide());
// Backdrop click to close
const backdrop = this.overlayElement.querySelector('#overlay-backdrop');
backdrop.addEventListener('click', () => this.hide());
// Escape key to close
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isVisible) {
this.hide();
}
});
// Random video button
const randomBtn = this.overlayElement.querySelector('.random-video-btn');
if (randomBtn) {
randomBtn.addEventListener('click', () => this.playRandomVideo());
}
// Prevent video container clicks from closing overlay
const videoContainer = this.overlayElement.querySelector('.overlay-video-container');
videoContainer.addEventListener('click', (e) => {
e.stopPropagation();
});
}
async show() {
if (this.isVisible) return;
// Add to DOM if not already there
if (!document.body.contains(this.overlayElement)) {
document.body.appendChild(this.overlayElement);
}
// Show overlay
this.overlayElement.style.display = 'flex';
this.isVisible = true;
// Fade in animation
setTimeout(() => {
this.overlayElement.classList.add('visible');
}, 10);
// Load a random video if none is currently loaded
if (!this.currentVideo) {
await this.playRandomVideo();
}
console.log('🎬 Overlay video player shown');
}
hide() {
if (!this.isVisible) return;
// Fade out animation
this.overlayElement.classList.remove('visible');
// Hide after animation
setTimeout(() => {
this.overlayElement.style.display = 'none';
this.isVisible = false;
// Pause video when hidden
if (this.videoElement) {
this.videoElement.pause();
}
}, 300);
console.log('🎬 Overlay video player hidden');
}
async playRandomVideo() {
try {
// Get all available videos
let allVideos = [];
if (window.desktopFileManager) {
allVideos = window.desktopFileManager.getAllVideos();
}
// Fallback to unified storage
if (allVideos.length === 0) {
const unifiedData = JSON.parse(localStorage.getItem('unifiedVideoLibrary') || '{}');
allVideos = unifiedData.allVideos || [];
}
// Fallback to legacy storage
if (allVideos.length === 0) {
const storedVideos = JSON.parse(localStorage.getItem('videoFiles') || '{}');
allVideos = Object.values(storedVideos).flat();
}
if (allVideos.length === 0) {
this.showError('No videos available for random playback');
return;
}
// Select random video
const randomIndex = Math.floor(Math.random() * allVideos.length);
const randomVideo = allVideos[randomIndex];
console.log(`🎲 Playing random video: ${randomVideo.name || randomVideo.title}`);
// Update video info
this.updateVideoInfo(randomVideo);
// Load and play the video
this.loadVideo(randomVideo.path || randomVideo.filePath, true);
this.currentVideo = randomVideo;
} catch (error) {
console.error('Error playing random video:', error);
this.showError('Error loading random video');
}
}
updateVideoInfo(video) {
const titleElement = this.overlayElement.querySelector('.video-title');
const infoElement = this.overlayElement.querySelector('.video-info');
const nameElement = this.overlayElement.querySelector('.video-name');
const videoName = video.name || video.title || 'Unknown Video';
const videoSize = this.formatFileSize(video.size || 0);
const videoDuration = this.formatDuration(video.duration || 0);
if (titleElement) {
titleElement.textContent = videoName;
}
if (infoElement) {
infoElement.textContent = `${videoSize}${videoDuration}`;
}
if (nameElement) {
nameElement.textContent = videoName;
}
// Update overlay window title
const overlayTitle = this.overlayElement.querySelector('.overlay-title');
if (overlayTitle) {
overlayTitle.textContent = `🎬 ${videoName}`;
}
}
showError(message) {
const titleElement = this.overlayElement.querySelector('.video-title');
const infoElement = this.overlayElement.querySelector('.video-info');
if (titleElement) {
titleElement.textContent = 'Error';
}
if (infoElement) {
infoElement.textContent = message;
}
}
formatFileSize(bytes) {
if (!bytes || bytes === 0) return '--';
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
}
formatDuration(seconds) {
if (!seconds || seconds === 0) return '--:--';
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(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')}`;
}
}
// Public methods
isOpen() {
return this.isVisible;
}
getCurrentVideo() {
return this.currentVideo;
}
// Static method to create and show overlay
static async showOverlay(options = {}) {
const overlay = new OverlayVideoPlayer(options);
await overlay.show();
return overlay;
}
}
// Export for global access
window.OverlayVideoPlayer = OverlayVideoPlayer;

View File

@ -0,0 +1,391 @@
/* Overlay Video Player Styles */
.video-overlay-popup {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10000;
display: none;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.video-overlay-popup.visible {
opacity: 1;
}
.overlay-backdrop {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(5px);
}
.overlay-window {
position: relative;
width: 90%;
max-width: 1200px;
max-height: 90%;
background: #1a1a1a;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
overflow: hidden;
transform: scale(0.9);
transition: transform 0.3s ease;
}
.video-overlay-popup.visible .overlay-window {
transform: scale(1);
}
.overlay-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px 20px;
background: #2a2a2a;
border-bottom: 1px solid #333;
}
.overlay-title {
margin: 0;
color: #fff;
font-size: 18px;
font-weight: 600;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: calc(100% - 40px);
}
.overlay-close-btn {
background: none;
border: none;
color: #999;
font-size: 24px;
cursor: pointer;
padding: 5px;
border-radius: 4px;
transition: all 0.2s ease;
line-height: 1;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.overlay-close-btn:hover {
background: #444;
color: #fff;
}
.overlay-video-container {
position: relative;
width: 100%;
aspect-ratio: 16/9;
background: #000;
overflow: hidden;
}
/* Ensure the video player inherits BaseVideoPlayer styles */
.overlay-video-container .main-video {
width: 100%;
height: 100%;
object-fit: contain;
}
.overlay-video-container .video-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.overlay-video-container:hover .video-overlay,
.overlay-video-container.paused .video-overlay {
opacity: 1;
}
.overlay-video-container .video-title {
color: #fff;
font-size: 24px;
font-weight: 600;
text-align: center;
margin-bottom: 8px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
max-width: 80%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.overlay-video-container .video-info {
color: #ccc;
font-size: 16px;
text-align: center;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
}
.overlay-video-container .play-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: auto;
}
.overlay-video-container .play-button-large {
background: rgba(255, 255, 255, 0.9);
border: none;
border-radius: 50%;
width: 80px;
height: 80px;
font-size: 32px;
color: #333;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.overlay-video-container .play-button-large:hover {
background: #fff;
transform: scale(1.1);
}
.overlay-video-container .video-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: #fff;
}
/* Video Controls for Overlay */
.overlay-video-container .video-controls {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
padding: 20px 15px 15px;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.overlay-video-container:hover .video-controls,
.overlay-video-container.paused .video-controls {
opacity: 1;
pointer-events: auto;
}
.overlay-video-container .progress-container {
margin-bottom: 12px;
}
.overlay-video-container .progress-bar {
position: relative;
height: 4px;
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
cursor: pointer;
margin-bottom: 8px;
}
.overlay-video-container .progress-filled {
height: 100%;
background: #e50914;
border-radius: 2px;
transition: width 0.1s ease;
}
.overlay-video-container .progress-thumb {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 12px;
height: 12px;
background: #e50914;
border-radius: 50%;
opacity: 0;
transition: opacity 0.2s ease;
}
.overlay-video-container .progress-bar:hover .progress-thumb {
opacity: 1;
}
.overlay-video-container .time-display {
display: flex;
justify-content: space-between;
color: #fff;
font-size: 12px;
font-family: 'Courier New', monospace;
}
.overlay-video-container .controls-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.overlay-video-container .controls-left,
.overlay-video-container .controls-right {
display: flex;
align-items: center;
gap: 10px;
}
.overlay-video-container .controls-center {
flex: 1;
text-align: center;
padding: 0 20px;
}
.overlay-video-container .video-name {
color: #fff;
font-size: 14px;
font-weight: 500;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.overlay-video-container .control-btn {
background: none;
border: none;
color: #fff;
font-size: 18px;
cursor: pointer;
padding: 8px;
border-radius: 4px;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
min-width: 36px;
height: 36px;
}
.overlay-video-container .control-btn:hover {
background: rgba(255, 255, 255, 0.1);
transform: scale(1.1);
}
.overlay-video-container .volume-control {
display: flex;
align-items: center;
gap: 8px;
}
.overlay-video-container .volume-slider {
width: 80px;
height: 4px;
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
outline: none;
cursor: pointer;
}
.overlay-video-container .volume-slider::-webkit-slider-thumb {
appearance: none;
width: 12px;
height: 12px;
background: #e50914;
border-radius: 50%;
cursor: pointer;
}
.overlay-video-container .volume-percentage {
color: #ccc;
font-size: 11px;
font-family: 'Courier New', monospace;
min-width: 30px;
text-align: right;
}
/* Random video button special styling */
.overlay-video-container .random-video-btn {
background: rgba(255, 215, 0, 0.1);
border: 1px solid rgba(255, 215, 0, 0.3);
}
.overlay-video-container .random-video-btn:hover {
background: rgba(255, 215, 0, 0.2);
border-color: rgba(255, 215, 0, 0.5);
}
/* Loading spinner for overlay */
.overlay-video-container .loading-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(255, 255, 255, 0.2);
border-top: 4px solid #e50914;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive design */
@media (max-width: 768px) {
.overlay-window {
width: 95%;
max-height: 95%;
}
.overlay-header {
padding: 12px 15px;
}
.overlay-title {
font-size: 16px;
}
.overlay-video-container .video-title {
font-size: 20px;
}
.overlay-video-container .controls-center {
padding: 0 10px;
}
.overlay-video-container .volume-slider {
width: 60px;
}
}
@media (max-width: 480px) {
.overlay-video-container .controls-center {
display: none; /* Hide video name on very small screens */
}
.overlay-video-container .volume-percentage {
display: none; /* Hide percentage on very small screens */
}
}