Subham9126 commited on
Commit
e9ae44e
·
verified ·
1 Parent(s): 2874a3f

Update script.js

Browse files
Files changed (1) hide show
  1. script.js +169 -349
script.js CHANGED
@@ -15,9 +15,6 @@ document.addEventListener('DOMContentLoaded', () => {
15
  const pagination = document.getElementById('pagination');
16
  const visibleCount = document.getElementById('visibleCount');
17
  const totalCount = document.getElementById('totalCount');
18
- // *** NEW: Player Loading Elements ***
19
- const playerLoadingIndicator = document.getElementById('playerLoadingIndicator');
20
- const playerLoadPercent = document.getElementById('playerLoadPercent');
21
 
22
  // State
23
  let videos = [];
@@ -51,7 +48,7 @@ document.addEventListener('DOMContentLoaded', () => {
51
  }
52
 
53
  const data = await response.json();
54
-
55
  // Check if data has the expected structure
56
  if (!data.videos || !Array.isArray(data.videos)) {
57
  throw new Error('Invalid database format: missing "videos" array');
@@ -116,15 +113,15 @@ document.addEventListener('DOMContentLoaded', () => {
116
  function handleSearch() {
117
  const searchTerm = searchInput.value.toLowerCase().trim();
118
  clearSearchBtn.style.display = searchTerm ? 'block' : 'none';
119
-
120
  if (searchTerm === '') {
121
  filteredVideos = [...videos];
122
  } else {
123
- filteredVideos = videos.filter(video =>
124
  video.title.toLowerCase().includes(searchTerm)
125
  );
126
  }
127
-
128
  currentPage = 1;
129
  updateVideoCount();
130
  renderPagination();
@@ -147,7 +144,7 @@ document.addEventListener('DOMContentLoaded', () => {
147
  const start = (currentPage - 1) * videosPerPage;
148
  const end = Math.min(start + videosPerPage, filteredVideos.length);
149
  const count = Math.min(videosPerPage, filteredVideos.length - start);
150
-
151
  visibleCount.textContent = count > 0 ? end - start : 0;
152
  totalCount.textContent = filteredVideos.length;
153
  }
@@ -155,14 +152,14 @@ document.addEventListener('DOMContentLoaded', () => {
155
  // Set view mode (grid or list)
156
  function setViewMode(mode) {
157
  viewMode = mode;
158
-
159
  // Update active button
160
  gridViewBtn.classList.toggle('active', mode === 'grid');
161
  listViewBtn.classList.toggle('active', mode === 'list');
162
-
163
  // Update grid class
164
  videoGrid.classList.toggle('list-view', mode === 'list');
165
-
166
  // Re-render to apply changes
167
  renderVideos();
168
  }
@@ -171,7 +168,7 @@ document.addEventListener('DOMContentLoaded', () => {
171
  function renderVideos() {
172
  loadingIndicator.style.display = 'none';
173
  videoGrid.innerHTML = '';
174
-
175
  if (filteredVideos.length === 0) {
176
  videoGrid.innerHTML = `
177
  <div class="error-container" style="grid-column: 1 / -1;">
@@ -179,32 +176,30 @@ document.addEventListener('DOMContentLoaded', () => {
179
  <p>No videos found matching your search.</p>
180
  </div>
181
  `;
182
- updateVideoCount(); // Ensure count shows 0 of 0
183
  return;
184
  }
185
-
186
  // Calculate pagination
187
  const start = (currentPage - 1) * videosPerPage;
188
  const end = Math.min(start + videosPerPage, filteredVideos.length);
189
  const currentPageVideos = filteredVideos.slice(start, end);
190
- updateVideoCount(); // Update count based on current page
191
-
192
  // Create video cards
193
  currentPageVideos.forEach((video, index) => {
194
  const videoCard = document.createElement('div');
195
  videoCard.className = `video-card ${viewMode === 'list' ? 'list-view' : ''}`;
196
-
197
  // Determine if URL is from Hugging Face
198
  const isHuggingFaceUrl = video.url.includes('huggingface.co');
199
-
200
  // For HF URLs, append download=true if not already present
201
  let videoUrl = video.url;
202
  if (isHuggingFaceUrl && !videoUrl.includes('download=true')) {
203
- videoUrl = videoUrl.includes('?') ?
204
- `${videoUrl}&download=true` :
205
  `${videoUrl}?download=true`;
206
  }
207
-
208
  videoCard.innerHTML = `
209
  <div class="video-thumbnail">
210
  <div class="thumbnail-placeholder">
@@ -218,11 +213,11 @@ document.addEventListener('DOMContentLoaded', () => {
218
  <h3 class="video-title">${video.title}</h3>
219
  </div>
220
  `;
221
-
222
  videoCard.addEventListener('click', () => {
223
  playVideo(video);
224
  });
225
-
226
  videoGrid.appendChild(videoCard);
227
  });
228
  }
@@ -230,13 +225,13 @@ document.addEventListener('DOMContentLoaded', () => {
230
  // Render pagination controls
231
  function renderPagination() {
232
  pagination.innerHTML = '';
233
-
234
  if (filteredVideos.length <= videosPerPage) {
235
  return; // No pagination needed
236
  }
237
-
238
  const totalPages = Math.ceil(filteredVideos.length / videosPerPage);
239
-
240
  // Previous button
241
  const prevBtn = document.createElement('button');
242
  prevBtn.className = `page-btn ${currentPage === 1 ? 'disabled' : ''}`;
@@ -248,7 +243,7 @@ document.addEventListener('DOMContentLoaded', () => {
248
  }
249
  });
250
  pagination.appendChild(prevBtn);
251
-
252
  // Page numbers with ellipsis
253
  const renderPageButton = (pageNum) => {
254
  const pageBtn = document.createElement('button');
@@ -257,48 +252,35 @@ document.addEventListener('DOMContentLoaded', () => {
257
  pageBtn.addEventListener('click', () => goToPage(pageNum));
258
  pagination.appendChild(pageBtn);
259
  };
260
-
261
  // Determine visible page range
262
- const delta = 1; // Reduced delta for smaller screens / cleaner look
263
  const range = [];
264
  const rangeWithDots = [];
265
  let l;
266
-
267
- range.push(1); // Always show first page
268
-
269
- // Calculate boundaries for center range
270
- let left = currentPage - delta;
271
- let right = currentPage + delta;
272
-
273
- if (left <= 1) left = 2;
274
- if (right >= totalPages) right = totalPages - 1;
275
-
276
- // Add pages around current page
277
- for (let i = left; i <= right; i++) {
278
- if (i > 1 && i < totalPages) { // Avoid duplicates if delta overlaps 1 or totalPages
279
- range.push(i);
280
  }
281
  }
282
-
283
- if (totalPages > 1) { // Always show last page if different from first
284
- range.push(totalPages);
285
- }
286
- range.sort((a,b) => a - b); // Ensure order
287
-
288
- // Add ellipsis logic
289
  for (let i of range) {
290
  if (l) {
291
  if (i - l === 2) {
292
- rangeWithDots.push(l + 1); // Add missing page
293
- } else if (i - l > 1) {
294
- rangeWithDots.push('...'); // Add ellipsis
295
  }
296
  }
297
  rangeWithDots.push(i);
298
  l = i;
299
  }
300
-
301
- // Render the determined buttons/ellipsis
302
  rangeWithDots.forEach(item => {
303
  if (item === '...') {
304
  const ellipsis = document.createElement('span');
@@ -309,7 +291,7 @@ document.addEventListener('DOMContentLoaded', () => {
309
  renderPageButton(item);
310
  }
311
  });
312
-
313
  // Next button
314
  const nextBtn = document.createElement('button');
315
  nextBtn.className = `page-btn ${currentPage === totalPages ? 'disabled' : ''}`;
@@ -326,398 +308,236 @@ document.addEventListener('DOMContentLoaded', () => {
326
  // Navigate to a specific page
327
  function goToPage(pageNum) {
328
  currentPage = pageNum;
329
- renderPagination(); // Re-render pagination to update active state/ellipsis
330
- renderVideos(); // Re-render videos for the new page
331
- updateVideoCount(); // Update counts *after* rendering new page
332
-
333
  // Scroll to top of video grid
334
- // Use a slight delay if needed for smooth scroll to work reliably after render
335
- setTimeout(() => {
336
- const controlsBar = document.querySelector('.controls-bar');
337
- if (controlsBar) {
338
- controlsBar.scrollIntoView({ behavior: 'smooth', block: 'start' });
339
- } else {
340
- videoGrid.scrollIntoView({ behavior: 'smooth', block: 'start' });
341
- }
342
- }, 50);
343
  }
344
 
345
  // Play video in modal
346
  function playVideo(video) {
347
- // Clean up any existing player *before* setting up the new one
348
  cleanupPlayer();
349
-
350
- console.log("Setting up new video:", video.title);
351
-
352
  // Update modal title
353
  modalVideoTitle.textContent = video.title;
354
-
355
  // Determine if URL is from Hugging Face
356
  const isHuggingFaceUrl = video.url.includes('huggingface.co');
357
-
358
  // For HF URLs, append download=true if not already present
359
  let videoUrl = video.url;
360
  if (isHuggingFaceUrl && !videoUrl.includes('download=true')) {
361
- videoUrl = videoUrl.includes('?') ?
362
- `${videoUrl}&download=true` :
363
  `${videoUrl}?download=true`;
364
  }
365
-
366
- // Set up video player source
367
  videoPlayer.src = videoUrl;
368
-
369
  // Initialize Plyr
370
- try {
371
- currentPlayer = new Plyr(videoPlayer, {
372
- controls: [
373
- 'play-large', 'play', 'progress', 'current-time', 'mute',
374
- 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'
375
- ],
376
- keyboard: { focused: true, global: false },
377
- tooltips: { controls: true, seek: true },
378
- captions: { active: true, language: 'auto', update: true }
379
- });
380
- console.log("Plyr initialized.");
381
- } catch (error) {
382
- console.error("Error initializing Plyr:", error);
383
- showToast('Failed to initialize video player.', 'error');
384
- closeVideoModal(); // Close modal if Plyr fails
385
- return;
386
- }
387
-
388
- // --- Buffering Event Listeners ---
389
- const handleVideoWaiting = () => {
390
- console.log("Video waiting (buffering)...");
391
- playerLoadPercent.textContent = 'Buffering...'; // Update text during wait
392
- playerLoadingIndicator.style.display = 'flex';
393
- };
394
-
395
- const handleVideoPlaying = () => {
396
- console.log("Video playing.");
397
- playerLoadingIndicator.style.display = 'none';
398
- };
399
-
400
- const handleVideoProgress = () => {
401
- if (!videoPlayer.duration || !isFinite(videoPlayer.duration)) {
402
- playerLoadPercent.textContent = 'Loading...'; // Or 'Live' if applicable
403
- return; // Avoid NaN if duration isn't loaded or is infinite
404
- }
405
-
406
- try {
407
- let bufferedEnd = 0;
408
- if (videoPlayer.buffered.length > 0) {
409
- // Get the end time of the last buffered time range
410
- bufferedEnd = videoPlayer.buffered.end(videoPlayer.buffered.length - 1);
411
- }
412
-
413
- const loadPercent = Math.round((bufferedEnd / videoPlayer.duration) * 100);
414
- // Only update if indicator is visible (or always, depending on preference)
415
- // if (playerLoadingIndicator.style.display === 'flex') {
416
- playerLoadPercent.textContent = `Loading ${loadPercent}%`;
417
- // }
418
- // console.log(`Buffered: ${bufferedEnd.toFixed(2)}s / ${videoPlayer.duration.toFixed(2)}s (${loadPercent}%)`);
419
- } catch (e) {
420
- console.warn("Error calculating buffer progress:", e);
421
- playerLoadPercent.textContent = 'Loading...';
422
- }
423
- };
424
-
425
- // Store handlers on the element for easy removal later
426
- videoPlayer._eventListeners = {
427
- waiting: handleVideoWaiting,
428
- playing: handleVideoPlaying,
429
- progress: handleVideoProgress
430
- };
431
-
432
- videoPlayer.addEventListener('waiting', videoPlayer._eventListeners.waiting);
433
- videoPlayer.addEventListener('playing', videoPlayer._eventListeners.playing);
434
- videoPlayer.addEventListener('progress', videoPlayer._eventListeners.progress);
435
-
436
- // Also hide indicator initially if player emits 'canplay' or ready early on
437
- // Use Plyr's 'ready' event as it's more reliable after initialization
438
- if (currentPlayer) {
439
- currentPlayer.once('ready', () => {
440
- console.log("Plyr ready event fired.");
441
- // Duration might be available here, trigger initial progress update
442
- handleVideoProgress();
443
- // If it's not already playing and not buffering, hide loader
444
- if (!videoPlayer.paused && videoPlayer.readyState >= 3) { // HAVE_FUTURE_DATA or more
445
- playerLoadingIndicator.style.display = 'none';
446
- }
447
- });
448
- // Handle cases where video fails to load within Plyr
449
- currentPlayer.once('error', (event) => {
450
- console.error("Plyr error event:", event.detail.plyr.source);
451
- showToast('Error loading video source.', 'error');
452
- playerLoadingIndicator.style.display = 'none'; // Hide indicator on error
453
- });
454
- }
455
-
456
-
457
- // Reset loading text for the new video
458
- playerLoadPercent.textContent = 'Loading 0%';
459
-
460
  // Set up double-tap controls for mobile
461
  setupDoubleTapControls();
462
-
463
  // Show modal
464
  videoModal.style.display = 'block';
465
  document.body.style.overflow = 'hidden'; // Prevent scrolling while modal is open
466
-
467
- // Attempt to Play the video (with error handling)
468
  try {
469
- // Show loading indicator immediately before play attempt
470
- playerLoadingIndicator.style.display = 'flex';
471
- playerLoadPercent.textContent = 'Loading 0%'; // Reset text
472
-
473
- const playPromise = currentPlayer.play();
474
-
475
- if (playPromise !== undefined) {
476
- playPromise.then(_ => {
477
- console.log("Autoplay successful or initiated.");
478
- // 'playing' event will hide the indicator
479
- }).catch(error => {
480
- console.warn('Autoplay prevented:', error);
481
- // Autoplay was prevented. User needs to click play.
482
- // The indicator is already shown. 'playing' event will hide it when user clicks.
483
- showToast('Click play to start video', 'info');
484
- // Ensure indicator hides if playing starts manually after prevention
485
- currentPlayer.once('playing', handleVideoPlaying);
486
- });
487
- }
488
  } catch (error) {
489
- console.error('Error initiating video play:', error);
490
  showToast('Error playing video. Please try again.', 'error');
491
- playerLoadingIndicator.style.display = 'none'; // Hide indicator on critical error
492
  }
493
  }
494
 
495
  // Set up double-tap controls for mobile
496
  function setupDoubleTapControls() {
497
- const playerElement = videoPlayer.closest('.plyr'); // Target the Plyr container
498
-
499
- if (!playerElement) {
500
- console.warn("Could not find Plyr container for double-tap setup.");
501
- return;
502
- }
503
-
504
  const handleTap = (e) => {
505
- if (!currentPlayer) return; // Don't do anything if player isn't active
506
-
507
  const now = Date.now();
508
- // Use clientX from the touch event
509
- const tapX = e.touches && e.touches.length > 0 ? e.touches[0].clientX : e.clientX;
510
-
511
- // Get bounding box relative to viewport
512
- const rect = playerElement.getBoundingClientRect();
513
- const playerWidth = rect.width;
514
- const tapPositionRelative = (tapX - rect.left) / playerWidth; // 0 to 1 position within player bounds
515
-
516
- // Check tap time and proximity
517
- if (now - lastTapTime < 300 && Math.abs(tapX - lastDoubleTapX) < doubleTapThreshold * (window.devicePixelRatio || 1)) {
518
  // Double tap detected
519
- e.preventDefault(); // Prevent zoom or other default actions
520
-
521
- // Ensure feedback is relative to the correct container
522
- const feedbackContainer = playerElement.querySelector('.plyr__video-wrapper') || playerElement;
523
-
524
- if (tapPositionRelative < 0.33) { // Use thirds for clearer zones
525
  // Double tap on left side - rewind
526
  currentPlayer.rewind(10);
527
- showControlFeedback(feedbackContainer, 'left', 'Rewind 10s');
528
- } else if (tapPositionRelative > 0.66) {
529
  // Double tap on right side - forward
530
  currentPlayer.forward(10);
531
- showControlFeedback(feedbackContainer, 'right', 'Forward 10s');
532
  } else {
533
  // Double tap in center - play/pause
534
- if (currentPlayer.playing) {
535
- currentPlayer.pause();
536
- } else {
537
- currentPlayer.play();
538
- }
539
- // Optional: show play/pause feedback
540
- // showControlFeedback(feedbackContainer, 'center', currentPlayer.playing ? 'Pause' : 'Play');
541
  }
542
- lastTapTime = 0; // Reset time after double tap to prevent triple tap issues
543
- lastDoubleTapX = 0;
544
- } else {
545
- // Single tap
546
- lastTapTime = now;
547
- lastDoubleTapX = tapX;
548
  }
 
 
 
549
  };
550
-
551
- // Add touch event listener - use touchstart for mobile responsiveness
552
- playerElement.addEventListener('touchstart', handleTap, { passive: false }); // Need passive: false to call preventDefault
553
-
554
  // Store for cleanup
555
  playerElement.handleTapFunc = handleTap;
556
  }
557
 
558
  // Visual feedback for touch controls
559
- function showControlFeedback(container, position, text) {
560
- // Remove any existing feedback first
561
- const existingFeedback = container.querySelector('.tap-feedback');
562
- if(existingFeedback) existingFeedback.remove();
563
-
564
  // Create a feedback element
565
  const feedback = document.createElement('div');
566
  feedback.className = `tap-feedback ${position}`;
567
  feedback.innerHTML = `
568
  <div class="feedback-icon">
569
- <i class="fas fa-${position === 'left' ? 'backward' : position === 'right' ? 'forward' : (currentPlayer && currentPlayer.playing ? 'pause' : 'play')} fa-lg"></i>
570
  </div>
571
  <div class="feedback-text">${text}</div>
572
  `;
573
-
574
- // Basic inline styles (better to define in CSS)
575
  feedback.style.position = 'absolute';
576
  feedback.style.top = '50%';
577
- feedback.style.transform = 'translateY(-50%) scale(0.8)'; // Start smaller
578
  feedback.style.color = 'white';
579
- feedback.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
580
- feedback.style.padding = '10px 15px';
581
- feedback.style.borderRadius = '8px'; // Less prominent than circle
582
  feedback.style.display = 'flex';
583
  feedback.style.flexDirection = 'column';
584
  feedback.style.alignItems = 'center';
585
- feedback.style.zIndex = '20'; // Ensure visibility over video/some controls
586
  feedback.style.opacity = '0';
587
- feedback.style.transition = 'opacity 0.5s ease-in-out, transform 0.5s ease-in-out';
588
- feedback.style.pointerEvents = 'none'; // Don't interfere with clicks
589
-
590
-
591
  if (position === 'left') {
592
  feedback.style.left = '15%';
593
- feedback.style.transform = 'translate(-50%, -50%) scale(0.8)';
594
  } else if (position === 'right') {
595
  feedback.style.right = '15%';
596
- feedback.style.transform = 'translate(50%, -50%) scale(0.8)'; // Correct transform for right
597
- } else { // Center (Play/Pause)
598
- feedback.style.left = '50%';
599
- feedback.style.transform = 'translate(-50%, -50%) scale(0.8)';
600
  }
601
-
602
- // Add to the specified container (e.g., plyr__video-wrapper)
603
- container.appendChild(feedback);
604
-
605
-
606
- // Animate in, then out
607
- requestAnimationFrame(() => {
608
- feedback.style.opacity = '1';
609
- feedback.style.transform = feedback.style.transform.replace('scale(0.8)', 'scale(1)'); // Grow to full size
610
- setTimeout(() => {
611
- feedback.style.opacity = '0';
612
- feedback.style.transform = feedback.style.transform.replace('scale(1)', 'scale(0.8)'); // Shrink out
613
- setTimeout(() => {
614
- feedback.remove();
615
- }, 500); // Match transition duration
616
- }, 600); // Duration visible
617
- });
 
 
 
 
 
 
618
  }
619
 
620
-
621
  // Close video modal and clean up player
622
  function closeVideoModal() {
623
  // Clean up player first
624
  cleanupPlayer();
625
-
626
  // Hide modal
627
  videoModal.style.display = 'none';
628
  document.body.style.overflow = ''; // Restore scrolling
629
  }
630
 
631
- // *** UPDATED: Clean up video player ***
632
  function cleanupPlayer() {
633
- console.log("Cleaning up player...");
634
-
635
- const playerElement = videoPlayer.closest('.plyr'); // Get Plyr wrapper
636
-
637
- // Remove buffering event listeners first
638
- if (videoPlayer._eventListeners) {
639
- console.log("Removing video event listeners...");
640
- videoPlayer.removeEventListener('waiting', videoPlayer._eventListeners.waiting);
641
- videoPlayer.removeEventListener('playing', videoPlayer._eventListeners.playing);
642
- videoPlayer.removeEventListener('progress', videoPlayer._eventListeners.progress);
643
- delete videoPlayer._eventListeners; // Clear the stored listeners
644
- // Hide player loading indicator just in case
645
- if (playerLoadingIndicator) {
646
- playerLoadingIndicator.style.display = 'none';
647
- }
648
- }
649
-
650
- // Remove touch event listeners from the player container
651
- if (playerElement && playerElement.handleTapFunc) {
652
- console.log("Removing touch listener...");
653
- playerElement.removeEventListener('touchstart', playerElement.handleTapFunc);
654
- delete playerElement.handleTapFunc;
655
- }
656
-
657
- // Destroy Plyr instance if it exists
658
  if (currentPlayer) {
 
659
  try {
660
- console.log("Destroying Plyr instance...");
661
- // Pause Plyr first (best practice)
662
  currentPlayer.pause();
663
- // Destroy Plyr instance
 
 
 
 
 
664
  currentPlayer.destroy();
665
- console.log("Plyr instance destroyed.");
666
  } catch (e) {
667
  console.warn('Error destroying Plyr instance:', e);
668
- } finally {
669
- currentPlayer = null; // Ensure reference is cleared even if destroy fails
670
  }
 
 
671
  }
672
-
673
- // Reset the native video element thoroughly
674
- console.log("Resetting native video element...");
675
- videoPlayer.pause(); // Ensure native element is paused
676
- videoPlayer.removeAttribute('src'); // Remove the source attribute
677
- // Clear any child <source> elements if they were added dynamically
678
- while (videoPlayer.firstChild) {
679
- videoPlayer.removeChild(videoPlayer.firstChild);
 
 
 
680
  }
681
- videoPlayer.src = ''; // Explicitly set src to empty string
682
- videoPlayer.load(); // Crucial: Resets the media element to its initial state
683
- console.log("Native video element reset.");
684
-
685
-
686
- // Ensure modal is ready for next video by clearing title etc. (optional but good practice)
687
- modalVideoTitle.textContent = 'Video Title'; // Reset title placeholder
688
-
689
- // Clear any existing feedback elements (double-tap indicators)
690
  const feedbacks = document.querySelectorAll('.tap-feedback');
691
  feedbacks.forEach(el => el.remove());
692
-
693
- console.log("Player cleanup complete.");
694
  }
695
 
696
  // Show toast message
697
  function showToast(message, type = 'info') {
698
  const toastContainer = document.getElementById('toastContainer');
699
-
700
  const toast = document.createElement('div');
701
  toast.className = `toast ${type}`;
702
  toast.textContent = message;
703
-
704
  toastContainer.appendChild(toast);
705
-
706
- // Trigger animation/transition
707
- requestAnimationFrame(() => {
708
- toast.style.opacity = 1;
709
- toast.style.transform = 'translateY(0)';
710
- });
711
-
712
-
713
- // Remove toast after duration
714
  setTimeout(() => {
715
- toast.style.opacity = 0;
716
- toast.style.transform = 'translateY(20px)';
717
- // Remove from DOM after transition
718
- toast.addEventListener('transitionend', () => toast.remove());
719
- // Fallback removal if transitionend doesn't fire
720
- setTimeout(() => toast.remove(), 500);
721
- }, 3000); // Keep toast visible for 3 seconds
722
  }
723
  });
 
15
  const pagination = document.getElementById('pagination');
16
  const visibleCount = document.getElementById('visibleCount');
17
  const totalCount = document.getElementById('totalCount');
 
 
 
18
 
19
  // State
20
  let videos = [];
 
48
  }
49
 
50
  const data = await response.json();
51
+
52
  // Check if data has the expected structure
53
  if (!data.videos || !Array.isArray(data.videos)) {
54
  throw new Error('Invalid database format: missing "videos" array');
 
113
  function handleSearch() {
114
  const searchTerm = searchInput.value.toLowerCase().trim();
115
  clearSearchBtn.style.display = searchTerm ? 'block' : 'none';
116
+
117
  if (searchTerm === '') {
118
  filteredVideos = [...videos];
119
  } else {
120
+ filteredVideos = videos.filter(video =>
121
  video.title.toLowerCase().includes(searchTerm)
122
  );
123
  }
124
+
125
  currentPage = 1;
126
  updateVideoCount();
127
  renderPagination();
 
144
  const start = (currentPage - 1) * videosPerPage;
145
  const end = Math.min(start + videosPerPage, filteredVideos.length);
146
  const count = Math.min(videosPerPage, filteredVideos.length - start);
147
+
148
  visibleCount.textContent = count > 0 ? end - start : 0;
149
  totalCount.textContent = filteredVideos.length;
150
  }
 
152
  // Set view mode (grid or list)
153
  function setViewMode(mode) {
154
  viewMode = mode;
155
+
156
  // Update active button
157
  gridViewBtn.classList.toggle('active', mode === 'grid');
158
  listViewBtn.classList.toggle('active', mode === 'list');
159
+
160
  // Update grid class
161
  videoGrid.classList.toggle('list-view', mode === 'list');
162
+
163
  // Re-render to apply changes
164
  renderVideos();
165
  }
 
168
  function renderVideos() {
169
  loadingIndicator.style.display = 'none';
170
  videoGrid.innerHTML = '';
171
+
172
  if (filteredVideos.length === 0) {
173
  videoGrid.innerHTML = `
174
  <div class="error-container" style="grid-column: 1 / -1;">
 
176
  <p>No videos found matching your search.</p>
177
  </div>
178
  `;
 
179
  return;
180
  }
181
+
182
  // Calculate pagination
183
  const start = (currentPage - 1) * videosPerPage;
184
  const end = Math.min(start + videosPerPage, filteredVideos.length);
185
  const currentPageVideos = filteredVideos.slice(start, end);
186
+
 
187
  // Create video cards
188
  currentPageVideos.forEach((video, index) => {
189
  const videoCard = document.createElement('div');
190
  videoCard.className = `video-card ${viewMode === 'list' ? 'list-view' : ''}`;
191
+
192
  // Determine if URL is from Hugging Face
193
  const isHuggingFaceUrl = video.url.includes('huggingface.co');
194
+
195
  // For HF URLs, append download=true if not already present
196
  let videoUrl = video.url;
197
  if (isHuggingFaceUrl && !videoUrl.includes('download=true')) {
198
+ videoUrl = videoUrl.includes('?') ?
199
+ `${videoUrl}&download=true` :
200
  `${videoUrl}?download=true`;
201
  }
202
+
203
  videoCard.innerHTML = `
204
  <div class="video-thumbnail">
205
  <div class="thumbnail-placeholder">
 
213
  <h3 class="video-title">${video.title}</h3>
214
  </div>
215
  `;
216
+
217
  videoCard.addEventListener('click', () => {
218
  playVideo(video);
219
  });
220
+
221
  videoGrid.appendChild(videoCard);
222
  });
223
  }
 
225
  // Render pagination controls
226
  function renderPagination() {
227
  pagination.innerHTML = '';
228
+
229
  if (filteredVideos.length <= videosPerPage) {
230
  return; // No pagination needed
231
  }
232
+
233
  const totalPages = Math.ceil(filteredVideos.length / videosPerPage);
234
+
235
  // Previous button
236
  const prevBtn = document.createElement('button');
237
  prevBtn.className = `page-btn ${currentPage === 1 ? 'disabled' : ''}`;
 
243
  }
244
  });
245
  pagination.appendChild(prevBtn);
246
+
247
  // Page numbers with ellipsis
248
  const renderPageButton = (pageNum) => {
249
  const pageBtn = document.createElement('button');
 
252
  pageBtn.addEventListener('click', () => goToPage(pageNum));
253
  pagination.appendChild(pageBtn);
254
  };
255
+
256
  // Determine visible page range
257
+ const delta = 2; // Number of pages to show on each side of current
258
  const range = [];
259
  const rangeWithDots = [];
260
  let l;
261
+
262
+ range.push(1);
263
+
264
+ for (let i = currentPage - delta; i <= currentPage + delta; i++) {
265
+ if (i > 1 && i < totalPages) {
266
+ range.push(i);
 
 
 
 
 
 
 
 
267
  }
268
  }
269
+
270
+ range.push(totalPages);
271
+
 
 
 
 
272
  for (let i of range) {
273
  if (l) {
274
  if (i - l === 2) {
275
+ rangeWithDots.push(l + 1);
276
+ } else if (i - l !== 1) {
277
+ rangeWithDots.push('...');
278
  }
279
  }
280
  rangeWithDots.push(i);
281
  l = i;
282
  }
283
+
 
284
  rangeWithDots.forEach(item => {
285
  if (item === '...') {
286
  const ellipsis = document.createElement('span');
 
291
  renderPageButton(item);
292
  }
293
  });
294
+
295
  // Next button
296
  const nextBtn = document.createElement('button');
297
  nextBtn.className = `page-btn ${currentPage === totalPages ? 'disabled' : ''}`;
 
308
  // Navigate to a specific page
309
  function goToPage(pageNum) {
310
  currentPage = pageNum;
311
+ updateVideoCount();
312
+ renderPagination();
313
+ renderVideos();
314
+
315
  // Scroll to top of video grid
316
+ videoGrid.scrollIntoView({ behavior: 'smooth' });
 
 
 
 
 
 
 
 
317
  }
318
 
319
  // Play video in modal
320
  function playVideo(video) {
321
+ // Clean up any existing player
322
  cleanupPlayer();
323
+
 
 
324
  // Update modal title
325
  modalVideoTitle.textContent = video.title;
326
+
327
  // Determine if URL is from Hugging Face
328
  const isHuggingFaceUrl = video.url.includes('huggingface.co');
329
+
330
  // For HF URLs, append download=true if not already present
331
  let videoUrl = video.url;
332
  if (isHuggingFaceUrl && !videoUrl.includes('download=true')) {
333
+ videoUrl = videoUrl.includes('?') ?
334
+ `${videoUrl}&download=true` :
335
  `${videoUrl}?download=true`;
336
  }
337
+
338
+ // Set up video player
339
  videoPlayer.src = videoUrl;
340
+
341
  // Initialize Plyr
342
+ currentPlayer = new Plyr(videoPlayer, {
343
+ controls: [
344
+ 'play-large', 'play', 'progress', 'current-time', 'mute',
345
+ 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'
346
+ ],
347
+ keyboard: { focused: true, global: false },
348
+ tooltips: { controls: true, seek: true },
349
+ captions: { active: true, language: 'auto', update: true }
350
+ });
351
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  // Set up double-tap controls for mobile
353
  setupDoubleTapControls();
354
+
355
  // Show modal
356
  videoModal.style.display = 'block';
357
  document.body.style.overflow = 'hidden'; // Prevent scrolling while modal is open
358
+
359
+ // Play the video (with error handling)
360
  try {
361
+ currentPlayer.play().catch(error => {
362
+ console.warn('Failed to autoplay video:', error);
363
+
364
+ // Check if it's a format issue
365
+ const fileExtension = videoUrl.split('.').pop().toLowerCase();
366
+ if (fileExtension === 'mkv') {
367
+ showToast('MKV format may not be fully supported in your browser.', 'warning');
368
+ }
369
+ });
 
 
 
 
 
 
 
 
 
 
370
  } catch (error) {
371
+ console.error('Error playing video:', error);
372
  showToast('Error playing video. Please try again.', 'error');
 
373
  }
374
  }
375
 
376
  // Set up double-tap controls for mobile
377
  function setupDoubleTapControls() {
378
+ const playerElement = videoPlayer.closest('.player-container');
379
+
 
 
 
 
 
380
  const handleTap = (e) => {
 
 
381
  const now = Date.now();
382
+ const tapX = e.touches ? e.touches[0].clientX : e.clientX;
383
+ const playerWidth = playerElement.offsetWidth;
384
+ const tapPosition = tapX / playerWidth; // 0 to 1 position
385
+
386
+ if (now - lastTapTime < 300 && Math.abs(tapX - lastDoubleTapX) < doubleTapThreshold) {
 
 
 
 
 
387
  // Double tap detected
388
+ e.preventDefault();
389
+
390
+ if (tapPosition < 0.3) {
 
 
 
391
  // Double tap on left side - rewind
392
  currentPlayer.rewind(10);
393
+ showControlFeedback('left', 'Rewind 10s');
394
+ } else if (tapPosition > 0.7) {
395
  // Double tap on right side - forward
396
  currentPlayer.forward(10);
397
+ showControlFeedback('right', 'Forward 10s');
398
  } else {
399
  // Double tap in center - play/pause
400
+ if (currentPlayer.playing) {
401
+ currentPlayer.pause();
402
+ } else {
403
+ currentPlayer.play();
404
+ }
 
 
405
  }
 
 
 
 
 
 
406
  }
407
+
408
+ lastTapTime = now;
409
+ lastDoubleTapX = tapX;
410
  };
411
+
412
+ // Add touch event listeners
413
+ playerElement.addEventListener('touchstart', handleTap);
414
+
415
  // Store for cleanup
416
  playerElement.handleTapFunc = handleTap;
417
  }
418
 
419
  // Visual feedback for touch controls
420
+ function showControlFeedback(position, text) {
421
+ const playerElement = videoPlayer.closest('.player-container');
422
+
 
 
423
  // Create a feedback element
424
  const feedback = document.createElement('div');
425
  feedback.className = `tap-feedback ${position}`;
426
  feedback.innerHTML = `
427
  <div class="feedback-icon">
428
+ <i class="fas fa-${position === 'left' ? 'backward' : position === 'right' ? 'forward' : 'play'}"></i>
429
  </div>
430
  <div class="feedback-text">${text}</div>
431
  `;
432
+
433
+ // Style it
434
  feedback.style.position = 'absolute';
435
  feedback.style.top = '50%';
436
+ feedback.style.transform = 'translateY(-50%)';
437
  feedback.style.color = 'white';
438
+ feedback.style.backgroundColor = 'rgba(0,0,0,0.6)';
439
+ feedback.style.padding = '15px';
440
+ feedback.style.borderRadius = '50%';
441
  feedback.style.display = 'flex';
442
  feedback.style.flexDirection = 'column';
443
  feedback.style.alignItems = 'center';
 
444
  feedback.style.opacity = '0';
445
+ feedback.style.animation = 'fadeInOut 1s ease-in-out';
446
+
 
 
447
  if (position === 'left') {
448
  feedback.style.left = '15%';
 
449
  } else if (position === 'right') {
450
  feedback.style.right = '15%';
451
+ } else {
452
+ feedback.style.left = '50%';
453
+ feedback.style.transform = 'translate(-50%, -50%)';
 
454
  }
455
+
456
+ // Add animation
457
+ const keyframes = `
458
+ @keyframes fadeInOut {
459
+ 0% { opacity: 0; }
460
+ 20% { opacity: 1; }
461
+ 80% { opacity: 1; }
462
+ 100% { opacity: 0; }
463
+ }
464
+ `;
465
+
466
+ const style = document.createElement('style');
467
+ style.textContent = keyframes;
468
+ document.head.appendChild(style);
469
+
470
+ // Add to player
471
+ playerElement.appendChild(feedback);
472
+
473
+ // Remove after animation
474
+ setTimeout(() => {
475
+ feedback.remove();
476
+ style.remove();
477
+ }, 1000);
478
  }
479
 
 
480
  // Close video modal and clean up player
481
  function closeVideoModal() {
482
  // Clean up player first
483
  cleanupPlayer();
484
+
485
  // Hide modal
486
  videoModal.style.display = 'none';
487
  document.body.style.overflow = ''; // Restore scrolling
488
  }
489
 
490
+ // Clean up video player
491
  function cleanupPlayer() {
492
+ // If a player instance exists, destroy it
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
  if (currentPlayer) {
494
+ // Make sure video is paused
495
  try {
 
 
496
  currentPlayer.pause();
497
+ } catch (e) {
498
+ console.warn('Error pausing player:', e);
499
+ }
500
+
501
+ // Try-catch to handle potential Plyr errors
502
+ try {
503
  currentPlayer.destroy();
 
504
  } catch (e) {
505
  console.warn('Error destroying Plyr instance:', e);
 
 
506
  }
507
+
508
+ currentPlayer = null;
509
  }
510
+
511
+ // Directly manipulate the video element as a failsafe
512
+ videoPlayer.pause();
513
+ videoPlayer.removeAttribute('src');
514
+ videoPlayer.load(); // Important: this resets the video element
515
+
516
+ // Remove any event listeners from the player container
517
+ const playerElement = videoPlayer.closest('.player-container');
518
+ if (playerElement && playerElement.handleTapFunc) {
519
+ playerElement.removeEventListener('touchstart', playerElement.handleTapFunc);
520
+ delete playerElement.handleTapFunc;
521
  }
522
+
523
+ // Clear any existing feedback elements
 
 
 
 
 
 
 
524
  const feedbacks = document.querySelectorAll('.tap-feedback');
525
  feedbacks.forEach(el => el.remove());
 
 
526
  }
527
 
528
  // Show toast message
529
  function showToast(message, type = 'info') {
530
  const toastContainer = document.getElementById('toastContainer');
531
+
532
  const toast = document.createElement('div');
533
  toast.className = `toast ${type}`;
534
  toast.textContent = message;
535
+
536
  toastContainer.appendChild(toast);
537
+
538
+ // Remove toast after animation completes
 
 
 
 
 
 
 
539
  setTimeout(() => {
540
+ toast.remove();
541
+ }, 3000);
 
 
 
 
 
542
  }
543
  });