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'); // 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 = `

No videos found matching your search.

`; return; } // Calculate pagination const start = (currentPage - 1) * videosPerPage; const end = Math.min(start + videosPerPage, filteredVideos.length); const currentPageVideos = filteredVideos.slice(start, end); // 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 = `

${video.title}

`; 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 = ''; 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 = 2; // Number of pages to show on each side of current const range = []; const rangeWithDots = []; let l; range.push(1); for (let i = currentPage - delta; i <= currentPage + delta; i++) { if (i > 1 && i < totalPages) { range.push(i); } } range.push(totalPages); for (let i of range) { if (l) { if (i - l === 2) { rangeWithDots.push(l + 1); } else if (i - l !== 1) { rangeWithDots.push('...'); } } rangeWithDots.push(i); l = i; } 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 = ''; 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; updateVideoCount(); renderPagination(); renderVideos(); // Scroll to top of video grid videoGrid.scrollIntoView({ behavior: 'smooth' }); } // Play video in modal function playVideo(video) { // Clean up any existing player cleanupPlayer(); // 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 videoPlayer.src = videoUrl; // Initialize Plyr 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 } }); // 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 // Play the video (with error handling) try { currentPlayer.play().catch(error => { console.warn('Failed to autoplay video:', error); // Check if it's a format issue const fileExtension = videoUrl.split('.').pop().toLowerCase(); if (fileExtension === 'mkv') { showToast('MKV format may not be fully supported in your browser.', 'warning'); } }); } catch (error) { console.error('Error playing video:', error); showToast('Error playing video. Please try again.', 'error'); } } // Set up double-tap controls for mobile function setupDoubleTapControls() { const playerElement = videoPlayer.closest('.player-container'); const handleTap = (e) => { const now = Date.now(); const tapX = e.touches ? e.touches[0].clientX : e.clientX; const playerWidth = playerElement.offsetWidth; const tapPosition = tapX / playerWidth; // 0 to 1 position if (now - lastTapTime < 300 && Math.abs(tapX - lastDoubleTapX) < doubleTapThreshold) { // Double tap detected e.preventDefault(); if (tapPosition < 0.3) { // Double tap on left side - rewind currentPlayer.rewind(10); showControlFeedback('left', 'Rewind 10s'); } else if (tapPosition > 0.7) { // Double tap on right side - forward currentPlayer.forward(10); showControlFeedback('right', 'Forward 10s'); } else { // Double tap in center - play/pause if (currentPlayer.playing) { currentPlayer.pause(); } else { currentPlayer.play(); } } } lastTapTime = now; lastDoubleTapX = tapX; }; // Add touch event listeners playerElement.addEventListener('touchstart', handleTap); // Store for cleanup playerElement.handleTapFunc = handleTap; } // Visual feedback for touch controls function showControlFeedback(position, text) { const playerElement = videoPlayer.closest('.player-container'); // Create a feedback element const feedback = document.createElement('div'); feedback.className = `tap-feedback ${position}`; feedback.innerHTML = `
${text}
`; // Style it feedback.style.position = 'absolute'; feedback.style.top = '50%'; feedback.style.transform = 'translateY(-50%)'; feedback.style.color = 'white'; feedback.style.backgroundColor = 'rgba(0,0,0,0.6)'; feedback.style.padding = '15px'; feedback.style.borderRadius = '50%'; feedback.style.display = 'flex'; feedback.style.flexDirection = 'column'; feedback.style.alignItems = 'center'; feedback.style.opacity = '0'; feedback.style.animation = 'fadeInOut 1s ease-in-out'; if (position === 'left') { feedback.style.left = '15%'; } else if (position === 'right') { feedback.style.right = '15%'; } else { feedback.style.left = '50%'; feedback.style.transform = 'translate(-50%, -50%)'; } // Add animation const keyframes = ` @keyframes fadeInOut { 0% { opacity: 0; } 20% { opacity: 1; } 80% { opacity: 1; } 100% { opacity: 0; } } `; const style = document.createElement('style'); style.textContent = keyframes; document.head.appendChild(style); // Add to player playerElement.appendChild(feedback); // Remove after animation setTimeout(() => { feedback.remove(); style.remove(); }, 1000); } // 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 } // Clean up video player function cleanupPlayer() { // If a player instance exists, destroy it if (currentPlayer) { // Make sure video is paused try { currentPlayer.pause(); } catch (e) { console.warn('Error pausing player:', e); } // Try-catch to handle potential Plyr errors try { currentPlayer.destroy(); } catch (e) { console.warn('Error destroying Plyr instance:', e); } currentPlayer = null; } // Directly manipulate the video element as a failsafe videoPlayer.pause(); videoPlayer.removeAttribute('src'); videoPlayer.load(); // Important: this resets the video element // Remove any event listeners from the player container const playerElement = videoPlayer.closest('.player-container'); if (playerElement && playerElement.handleTapFunc) { playerElement.removeEventListener('touchstart', playerElement.handleTapFunc); delete playerElement.handleTapFunc; } // Clear any existing feedback elements const feedbacks = document.querySelectorAll('.tap-feedback'); feedbacks.forEach(el => el.remove()); } // 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); // Remove toast after animation completes setTimeout(() => { toast.remove(); }, 3000); } });