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);
}
});