Subham9126 commited on
Commit
1dd9f6c
·
verified ·
1 Parent(s): d10c35a

Update script.js

Browse files
Files changed (1) hide show
  1. script.js +349 -169
script.js CHANGED
@@ -15,6 +15,9 @@ document.addEventListener('DOMContentLoaded', () => {
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,7 +51,7 @@ document.addEventListener('DOMContentLoaded', () => {
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,15 +116,15 @@ document.addEventListener('DOMContentLoaded', () => {
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,7 +147,7 @@ document.addEventListener('DOMContentLoaded', () => {
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,14 +155,14 @@ document.addEventListener('DOMContentLoaded', () => {
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,7 +171,7 @@ document.addEventListener('DOMContentLoaded', () => {
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,30 +179,32 @@ document.addEventListener('DOMContentLoaded', () => {
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,11 +218,11 @@ document.addEventListener('DOMContentLoaded', () => {
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,13 +230,13 @@ document.addEventListener('DOMContentLoaded', () => {
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,7 +248,7 @@ document.addEventListener('DOMContentLoaded', () => {
243
  }
244
  });
245
  pagination.appendChild(prevBtn);
246
-
247
  // Page numbers with ellipsis
248
  const renderPageButton = (pageNum) => {
249
  const pageBtn = document.createElement('button');
@@ -252,35 +257,48 @@ document.addEventListener('DOMContentLoaded', () => {
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,7 +309,7 @@ document.addEventListener('DOMContentLoaded', () => {
291
  renderPageButton(item);
292
  }
293
  });
294
-
295
  // Next button
296
  const nextBtn = document.createElement('button');
297
  nextBtn.className = `page-btn ${currentPage === totalPages ? 'disabled' : ''}`;
@@ -308,236 +326,398 @@ document.addEventListener('DOMContentLoaded', () => {
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
  });
 
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
  }
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
  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
  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
  // 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
  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
  <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
  <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
  // 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
  }
249
  });
250
  pagination.appendChild(prevBtn);
251
+
252
  // Page numbers with ellipsis
253
  const renderPageButton = (pageNum) => {
254
  const pageBtn = document.createElement('button');
 
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
  renderPageButton(item);
310
  }
311
  });
312
+
313
  // Next button
314
  const nextBtn = document.createElement('button');
315
  nextBtn.className = `page-btn ${currentPage === totalPages ? 'disabled' : ''}`;
 
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
  });