dfd / script.js
Subham9126's picture
Update script.js
1dd9f6c verified
Raw
History Blame
28.9 kB
document.addEventListener('DOMContentLoaded', () => {
// DOM Elements
const videoGrid = document.getElementById('videoGrid');
const searchInput = document.getElementById('searchInput');
const clearSearchBtn = document.getElementById('clearSearch');
const videoModal = document.getElementById('videoModal');
const videoPlayer = document.getElementById('videoPlayer');
const modalVideoTitle = document.getElementById('modalVideoTitle');
const closeModal = document.getElementById('closeModal');
const loadingIndicator = document.getElementById('loadingIndicator');
const errorMessage = document.getElementById('errorMessage');
const retryBtn = document.getElementById('retryBtn');
const gridViewBtn = document.getElementById('gridViewBtn');
const listViewBtn = document.getElementById('listViewBtn');
const pagination = document.getElementById('pagination');
const visibleCount = document.getElementById('visibleCount');
const totalCount = document.getElementById('totalCount');
// *** NEW: Player Loading Elements ***
const playerLoadingIndicator = document.getElementById('playerLoadingIndicator');
const playerLoadPercent = document.getElementById('playerLoadPercent');
// State
let videos = [];
let filteredVideos = [];
let currentPage = 1;
let videosPerPage = 12;
let currentPlayer = null;
let viewMode = 'grid';
let lastTapTime = 0;
let lastDoubleTapX = 0;
let doubleTapThreshold = 10; // px tolerance for tap position
// Initialize the app
init();
function init() {
fetchVideoData();
setupEventListeners();
}
// Fetch video data from JSON
async function fetchVideoData() {
try {
loadingIndicator.style.display = 'flex';
errorMessage.style.display = 'none';
videoGrid.innerHTML = '';
const response = await fetch('database.json');
if (!response.ok) {
throw new Error(`Failed to load database.json: ${response.status}`);
}
const data = await response.json();
// Check if data has the expected structure
if (!data.videos || !Array.isArray(data.videos)) {
throw new Error('Invalid database format: missing "videos" array');
}
// Filter out invalid entries
videos = data.videos.filter(video => {
if (!video.title || !video.url) {
console.warn('Invalid video entry:', video);
return false;
}
return true;
});
filteredVideos = [...videos];
updateVideoCount();
renderPagination();
renderVideos();
showToast('Videos loaded successfully!', 'success');
} catch (error) {
console.error('Error loading videos:', error);
loadingIndicator.style.display = 'none';
errorMessage.style.display = 'flex';
}
}
// Set up event listeners
function setupEventListeners() {
// Search functionality
searchInput.addEventListener('input', handleSearch);
clearSearchBtn.addEventListener('click', clearSearch);
// Modal controls
closeModal.addEventListener('click', closeVideoModal);
// Retry button
retryBtn.addEventListener('click', fetchVideoData);
// View toggle
gridViewBtn.addEventListener('click', () => setViewMode('grid'));
listViewBtn.addEventListener('click', () => setViewMode('list'));
// Handle clicks outside the modal to close it
window.addEventListener('click', (e) => {
if (e.target === videoModal) {
closeVideoModal();
}
});
// Keyboard controls for modal
window.addEventListener('keydown', (e) => {
if (videoModal.style.display === 'block') {
if (e.key === 'Escape') {
closeVideoModal();
}
}
});
}
// Handle search input
function handleSearch() {
const searchTerm = searchInput.value.toLowerCase().trim();
clearSearchBtn.style.display = searchTerm ? 'block' : 'none';
if (searchTerm === '') {
filteredVideos = [...videos];
} else {
filteredVideos = videos.filter(video =>
video.title.toLowerCase().includes(searchTerm)
);
}
currentPage = 1;
updateVideoCount();
renderPagination();
renderVideos();
}
// Clear search
function clearSearch() {
searchInput.value = '';
clearSearchBtn.style.display = 'none';
filteredVideos = [...videos];
currentPage = 1;
updateVideoCount();
renderPagination();
renderVideos();
}
// Update video count display
function updateVideoCount() {
const start = (currentPage - 1) * videosPerPage;
const end = Math.min(start + videosPerPage, filteredVideos.length);
const count = Math.min(videosPerPage, filteredVideos.length - start);
visibleCount.textContent = count > 0 ? end - start : 0;
totalCount.textContent = filteredVideos.length;
}
// Set view mode (grid or list)
function setViewMode(mode) {
viewMode = mode;
// Update active button
gridViewBtn.classList.toggle('active', mode === 'grid');
listViewBtn.classList.toggle('active', mode === 'list');
// Update grid class
videoGrid.classList.toggle('list-view', mode === 'list');
// Re-render to apply changes
renderVideos();
}
// Render videos in grid
function renderVideos() {
loadingIndicator.style.display = 'none';
videoGrid.innerHTML = '';
if (filteredVideos.length === 0) {
videoGrid.innerHTML = `
<div class="error-container" style="grid-column: 1 / -1;">
<i class="fas fa-search"></i>
<p>No videos found matching your search.</p>
</div>
`;
updateVideoCount(); // Ensure count shows 0 of 0
return;
}
// Calculate pagination
const start = (currentPage - 1) * videosPerPage;
const end = Math.min(start + videosPerPage, filteredVideos.length);
const currentPageVideos = filteredVideos.slice(start, end);
updateVideoCount(); // Update count based on current page
// Create video cards
currentPageVideos.forEach((video, index) => {
const videoCard = document.createElement('div');
videoCard.className = `video-card ${viewMode === 'list' ? 'list-view' : ''}`;
// Determine if URL is from Hugging Face
const isHuggingFaceUrl = video.url.includes('huggingface.co');
// For HF URLs, append download=true if not already present
let videoUrl = video.url;
if (isHuggingFaceUrl && !videoUrl.includes('download=true')) {
videoUrl = videoUrl.includes('?') ?
`${videoUrl}&download=true` :
`${videoUrl}?download=true`;
}
videoCard.innerHTML = `
<div class="video-thumbnail">
<div class="thumbnail-placeholder">
<i class="fas fa-film fa-2x"></i>
</div>
<div class="play-icon">
<i class="fas fa-play fa-lg"></i>
</div>
</div>
<div class="video-info">
<h3 class="video-title">${video.title}</h3>
</div>
`;
videoCard.addEventListener('click', () => {
playVideo(video);
});
videoGrid.appendChild(videoCard);
});
}
// Render pagination controls
function renderPagination() {
pagination.innerHTML = '';
if (filteredVideos.length <= videosPerPage) {
return; // No pagination needed
}
const totalPages = Math.ceil(filteredVideos.length / videosPerPage);
// Previous button
const prevBtn = document.createElement('button');
prevBtn.className = `page-btn ${currentPage === 1 ? 'disabled' : ''}`;
prevBtn.innerHTML = '<i class="fas fa-chevron-left"></i>';
prevBtn.disabled = currentPage === 1;
prevBtn.addEventListener('click', () => {
if (currentPage > 1) {
goToPage(currentPage - 1);
}
});
pagination.appendChild(prevBtn);
// Page numbers with ellipsis
const renderPageButton = (pageNum) => {
const pageBtn = document.createElement('button');
pageBtn.className = `page-btn ${currentPage === pageNum ? 'active' : ''}`;
pageBtn.textContent = pageNum;
pageBtn.addEventListener('click', () => goToPage(pageNum));
pagination.appendChild(pageBtn);
};
// Determine visible page range
const delta = 1; // Reduced delta for smaller screens / cleaner look
const range = [];
const rangeWithDots = [];
let l;
range.push(1); // Always show first page
// Calculate boundaries for center range
let left = currentPage - delta;
let right = currentPage + delta;
if (left <= 1) left = 2;
if (right >= totalPages) right = totalPages - 1;
// Add pages around current page
for (let i = left; i <= right; i++) {
if (i > 1 && i < totalPages) { // Avoid duplicates if delta overlaps 1 or totalPages
range.push(i);
}
}
if (totalPages > 1) { // Always show last page if different from first
range.push(totalPages);
}
range.sort((a,b) => a - b); // Ensure order
// Add ellipsis logic
for (let i of range) {
if (l) {
if (i - l === 2) {
rangeWithDots.push(l + 1); // Add missing page
} else if (i - l > 1) {
rangeWithDots.push('...'); // Add ellipsis
}
}
rangeWithDots.push(i);
l = i;
}
// Render the determined buttons/ellipsis
rangeWithDots.forEach(item => {
if (item === '...') {
const ellipsis = document.createElement('span');
ellipsis.className = 'page-btn disabled';
ellipsis.textContent = '...';
pagination.appendChild(ellipsis);
} else {
renderPageButton(item);
}
});
// Next button
const nextBtn = document.createElement('button');
nextBtn.className = `page-btn ${currentPage === totalPages ? 'disabled' : ''}`;
nextBtn.innerHTML = '<i class="fas fa-chevron-right"></i>';
nextBtn.disabled = currentPage === totalPages;
nextBtn.addEventListener('click', () => {
if (currentPage < totalPages) {
goToPage(currentPage + 1);
}
});
pagination.appendChild(nextBtn);
}
// Navigate to a specific page
function goToPage(pageNum) {
currentPage = pageNum;
renderPagination(); // Re-render pagination to update active state/ellipsis
renderVideos(); // Re-render videos for the new page
updateVideoCount(); // Update counts *after* rendering new page
// Scroll to top of video grid
// Use a slight delay if needed for smooth scroll to work reliably after render
setTimeout(() => {
const controlsBar = document.querySelector('.controls-bar');
if (controlsBar) {
controlsBar.scrollIntoView({ behavior: 'smooth', block: 'start' });
} else {
videoGrid.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, 50);
}
// Play video in modal
function playVideo(video) {
// Clean up any existing player *before* setting up the new one
cleanupPlayer();
console.log("Setting up new video:", video.title);
// Update modal title
modalVideoTitle.textContent = video.title;
// Determine if URL is from Hugging Face
const isHuggingFaceUrl = video.url.includes('huggingface.co');
// For HF URLs, append download=true if not already present
let videoUrl = video.url;
if (isHuggingFaceUrl && !videoUrl.includes('download=true')) {
videoUrl = videoUrl.includes('?') ?
`${videoUrl}&download=true` :
`${videoUrl}?download=true`;
}
// Set up video player source
videoPlayer.src = videoUrl;
// Initialize Plyr
try {
currentPlayer = new Plyr(videoPlayer, {
controls: [
'play-large', 'play', 'progress', 'current-time', 'mute',
'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'
],
keyboard: { focused: true, global: false },
tooltips: { controls: true, seek: true },
captions: { active: true, language: 'auto', update: true }
});
console.log("Plyr initialized.");
} catch (error) {
console.error("Error initializing Plyr:", error);
showToast('Failed to initialize video player.', 'error');
closeVideoModal(); // Close modal if Plyr fails
return;
}
// --- Buffering Event Listeners ---
const handleVideoWaiting = () => {
console.log("Video waiting (buffering)...");
playerLoadPercent.textContent = 'Buffering...'; // Update text during wait
playerLoadingIndicator.style.display = 'flex';
};
const handleVideoPlaying = () => {
console.log("Video playing.");
playerLoadingIndicator.style.display = 'none';
};
const handleVideoProgress = () => {
if (!videoPlayer.duration || !isFinite(videoPlayer.duration)) {
playerLoadPercent.textContent = 'Loading...'; // Or 'Live' if applicable
return; // Avoid NaN if duration isn't loaded or is infinite
}
try {
let bufferedEnd = 0;
if (videoPlayer.buffered.length > 0) {
// Get the end time of the last buffered time range
bufferedEnd = videoPlayer.buffered.end(videoPlayer.buffered.length - 1);
}
const loadPercent = Math.round((bufferedEnd / videoPlayer.duration) * 100);
// Only update if indicator is visible (or always, depending on preference)
// if (playerLoadingIndicator.style.display === 'flex') {
playerLoadPercent.textContent = `Loading ${loadPercent}%`;
// }
// console.log(`Buffered: ${bufferedEnd.toFixed(2)}s / ${videoPlayer.duration.toFixed(2)}s (${loadPercent}%)`);
} catch (e) {
console.warn("Error calculating buffer progress:", e);
playerLoadPercent.textContent = 'Loading...';
}
};
// Store handlers on the element for easy removal later
videoPlayer._eventListeners = {
waiting: handleVideoWaiting,
playing: handleVideoPlaying,
progress: handleVideoProgress
};
videoPlayer.addEventListener('waiting', videoPlayer._eventListeners.waiting);
videoPlayer.addEventListener('playing', videoPlayer._eventListeners.playing);
videoPlayer.addEventListener('progress', videoPlayer._eventListeners.progress);
// Also hide indicator initially if player emits 'canplay' or ready early on
// Use Plyr's 'ready' event as it's more reliable after initialization
if (currentPlayer) {
currentPlayer.once('ready', () => {
console.log("Plyr ready event fired.");
// Duration might be available here, trigger initial progress update
handleVideoProgress();
// If it's not already playing and not buffering, hide loader
if (!videoPlayer.paused && videoPlayer.readyState >= 3) { // HAVE_FUTURE_DATA or more
playerLoadingIndicator.style.display = 'none';
}
});
// Handle cases where video fails to load within Plyr
currentPlayer.once('error', (event) => {
console.error("Plyr error event:", event.detail.plyr.source);
showToast('Error loading video source.', 'error');
playerLoadingIndicator.style.display = 'none'; // Hide indicator on error
});
}
// Reset loading text for the new video
playerLoadPercent.textContent = 'Loading 0%';
// Set up double-tap controls for mobile
setupDoubleTapControls();
// Show modal
videoModal.style.display = 'block';
document.body.style.overflow = 'hidden'; // Prevent scrolling while modal is open
// Attempt to Play the video (with error handling)
try {
// Show loading indicator immediately before play attempt
playerLoadingIndicator.style.display = 'flex';
playerLoadPercent.textContent = 'Loading 0%'; // Reset text
const playPromise = currentPlayer.play();
if (playPromise !== undefined) {
playPromise.then(_ => {
console.log("Autoplay successful or initiated.");
// 'playing' event will hide the indicator
}).catch(error => {
console.warn('Autoplay prevented:', error);
// Autoplay was prevented. User needs to click play.
// The indicator is already shown. 'playing' event will hide it when user clicks.
showToast('Click play to start video', 'info');
// Ensure indicator hides if playing starts manually after prevention
currentPlayer.once('playing', handleVideoPlaying);
});
}
} catch (error) {
console.error('Error initiating video play:', error);
showToast('Error playing video. Please try again.', 'error');
playerLoadingIndicator.style.display = 'none'; // Hide indicator on critical error
}
}
// Set up double-tap controls for mobile
function setupDoubleTapControls() {
const playerElement = videoPlayer.closest('.plyr'); // Target the Plyr container
if (!playerElement) {
console.warn("Could not find Plyr container for double-tap setup.");
return;
}
const handleTap = (e) => {
if (!currentPlayer) return; // Don't do anything if player isn't active
const now = Date.now();
// Use clientX from the touch event
const tapX = e.touches && e.touches.length > 0 ? e.touches[0].clientX : e.clientX;
// Get bounding box relative to viewport
const rect = playerElement.getBoundingClientRect();
const playerWidth = rect.width;
const tapPositionRelative = (tapX - rect.left) / playerWidth; // 0 to 1 position within player bounds
// Check tap time and proximity
if (now - lastTapTime < 300 && Math.abs(tapX - lastDoubleTapX) < doubleTapThreshold * (window.devicePixelRatio || 1)) {
// Double tap detected
e.preventDefault(); // Prevent zoom or other default actions
// Ensure feedback is relative to the correct container
const feedbackContainer = playerElement.querySelector('.plyr__video-wrapper') || playerElement;
if (tapPositionRelative < 0.33) { // Use thirds for clearer zones
// Double tap on left side - rewind
currentPlayer.rewind(10);
showControlFeedback(feedbackContainer, 'left', 'Rewind 10s');
} else if (tapPositionRelative > 0.66) {
// Double tap on right side - forward
currentPlayer.forward(10);
showControlFeedback(feedbackContainer, 'right', 'Forward 10s');
} else {
// Double tap in center - play/pause
if (currentPlayer.playing) {
currentPlayer.pause();
} else {
currentPlayer.play();
}
// Optional: show play/pause feedback
// showControlFeedback(feedbackContainer, 'center', currentPlayer.playing ? 'Pause' : 'Play');
}
lastTapTime = 0; // Reset time after double tap to prevent triple tap issues
lastDoubleTapX = 0;
} else {
// Single tap
lastTapTime = now;
lastDoubleTapX = tapX;
}
};
// Add touch event listener - use touchstart for mobile responsiveness
playerElement.addEventListener('touchstart', handleTap, { passive: false }); // Need passive: false to call preventDefault
// Store for cleanup
playerElement.handleTapFunc = handleTap;
}
// Visual feedback for touch controls
function showControlFeedback(container, position, text) {
// Remove any existing feedback first
const existingFeedback = container.querySelector('.tap-feedback');
if(existingFeedback) existingFeedback.remove();
// Create a feedback element
const feedback = document.createElement('div');
feedback.className = `tap-feedback ${position}`;
feedback.innerHTML = `
<div class="feedback-icon">
<i class="fas fa-${position === 'left' ? 'backward' : position === 'right' ? 'forward' : (currentPlayer && currentPlayer.playing ? 'pause' : 'play')} fa-lg"></i>
</div>
<div class="feedback-text">${text}</div>
`;
// Basic inline styles (better to define in CSS)
feedback.style.position = 'absolute';
feedback.style.top = '50%';
feedback.style.transform = 'translateY(-50%) scale(0.8)'; // Start smaller
feedback.style.color = 'white';
feedback.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
feedback.style.padding = '10px 15px';
feedback.style.borderRadius = '8px'; // Less prominent than circle
feedback.style.display = 'flex';
feedback.style.flexDirection = 'column';
feedback.style.alignItems = 'center';
feedback.style.zIndex = '20'; // Ensure visibility over video/some controls
feedback.style.opacity = '0';
feedback.style.transition = 'opacity 0.5s ease-in-out, transform 0.5s ease-in-out';
feedback.style.pointerEvents = 'none'; // Don't interfere with clicks
if (position === 'left') {
feedback.style.left = '15%';
feedback.style.transform = 'translate(-50%, -50%) scale(0.8)';
} else if (position === 'right') {
feedback.style.right = '15%';
feedback.style.transform = 'translate(50%, -50%) scale(0.8)'; // Correct transform for right
} else { // Center (Play/Pause)
feedback.style.left = '50%';
feedback.style.transform = 'translate(-50%, -50%) scale(0.8)';
}
// Add to the specified container (e.g., plyr__video-wrapper)
container.appendChild(feedback);
// Animate in, then out
requestAnimationFrame(() => {
feedback.style.opacity = '1';
feedback.style.transform = feedback.style.transform.replace('scale(0.8)', 'scale(1)'); // Grow to full size
setTimeout(() => {
feedback.style.opacity = '0';
feedback.style.transform = feedback.style.transform.replace('scale(1)', 'scale(0.8)'); // Shrink out
setTimeout(() => {
feedback.remove();
}, 500); // Match transition duration
}, 600); // Duration visible
});
}
// Close video modal and clean up player
function closeVideoModal() {
// Clean up player first
cleanupPlayer();
// Hide modal
videoModal.style.display = 'none';
document.body.style.overflow = ''; // Restore scrolling
}
// *** UPDATED: Clean up video player ***
function cleanupPlayer() {
console.log("Cleaning up player...");
const playerElement = videoPlayer.closest('.plyr'); // Get Plyr wrapper
// Remove buffering event listeners first
if (videoPlayer._eventListeners) {
console.log("Removing video event listeners...");
videoPlayer.removeEventListener('waiting', videoPlayer._eventListeners.waiting);
videoPlayer.removeEventListener('playing', videoPlayer._eventListeners.playing);
videoPlayer.removeEventListener('progress', videoPlayer._eventListeners.progress);
delete videoPlayer._eventListeners; // Clear the stored listeners
// Hide player loading indicator just in case
if (playerLoadingIndicator) {
playerLoadingIndicator.style.display = 'none';
}
}
// Remove touch event listeners from the player container
if (playerElement && playerElement.handleTapFunc) {
console.log("Removing touch listener...");
playerElement.removeEventListener('touchstart', playerElement.handleTapFunc);
delete playerElement.handleTapFunc;
}
// Destroy Plyr instance if it exists
if (currentPlayer) {
try {
console.log("Destroying Plyr instance...");
// Pause Plyr first (best practice)
currentPlayer.pause();
// Destroy Plyr instance
currentPlayer.destroy();
console.log("Plyr instance destroyed.");
} catch (e) {
console.warn('Error destroying Plyr instance:', e);
} finally {
currentPlayer = null; // Ensure reference is cleared even if destroy fails
}
}
// Reset the native video element thoroughly
console.log("Resetting native video element...");
videoPlayer.pause(); // Ensure native element is paused
videoPlayer.removeAttribute('src'); // Remove the source attribute
// Clear any child <source> elements if they were added dynamically
while (videoPlayer.firstChild) {
videoPlayer.removeChild(videoPlayer.firstChild);
}
videoPlayer.src = ''; // Explicitly set src to empty string
videoPlayer.load(); // Crucial: Resets the media element to its initial state
console.log("Native video element reset.");
// Ensure modal is ready for next video by clearing title etc. (optional but good practice)
modalVideoTitle.textContent = 'Video Title'; // Reset title placeholder
// Clear any existing feedback elements (double-tap indicators)
const feedbacks = document.querySelectorAll('.tap-feedback');
feedbacks.forEach(el => el.remove());
console.log("Player cleanup complete.");
}
// Show toast message
function showToast(message, type = 'info') {
const toastContainer = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
toastContainer.appendChild(toast);
// Trigger animation/transition
requestAnimationFrame(() => {
toast.style.opacity = 1;
toast.style.transform = 'translateY(0)';
});
// Remove toast after duration
setTimeout(() => {
toast.style.opacity = 0;
toast.style.transform = 'translateY(20px)';
// Remove from DOM after transition
toast.addEventListener('transitionend', () => toast.remove());
// Fallback removal if transitionend doesn't fire
setTimeout(() => toast.remove(), 500);
}, 3000); // Keep toast visible for 3 seconds
}
});