Spaces:
Running
Running
| 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 | |
| } | |
| }); |