Subham9126 commited on
Commit
c344676
·
verified ·
1 Parent(s): 0cb0646

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +136 -164
index.html CHANGED
@@ -3,13 +3,14 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>HF Video Library (Plyr + Controls)</title>
7
 
8
  <!-- Plyr CSS -->
9
  <link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css" />
10
 
11
  <style>
12
- /* --- Keep your existing base styles --- */
 
13
  * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
14
  body { background-color: #f5f5f5; padding: 20px; max-width: 1200px; margin: 0 auto; }
15
  .container { display: flex; flex-direction: column; gap: 20px; }
@@ -37,60 +38,20 @@
37
  .close-popup:hover { background-color: rgba(50,50,50,0.9); }
38
  .popup-title { color: white; padding: 12px 15px; font-size: 1.1em; background-color: #111; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; order: -1; }
39
  #videoPlayer { display: block; width: 100%; height: auto; max-height: 80vh; background-color: #000; }
40
- /* Plyr Customizations */
41
  :root { --plyr-color-main: #007bff; }
42
- /* Add styles for the double-tap visual feedback (optional) */
43
- .plyr__video-wrapper::after {
44
- content: '';
45
- position: absolute;
46
- top: 0; left: 50%;
47
- transform: translateX(-50%);
48
- width: 0; height: 100%;
49
- background: rgba(255, 255, 255, 0.15);
50
- border-radius: 50%;
51
- opacity: 0;
52
- transition: width 0.2s ease-out, opacity 0.2s ease-out;
53
- pointer-events: none; /* Important */
54
- }
55
- .plyr__video-wrapper.seeking-forward::after {
56
- left: auto; right: 15%; transform: translateX(0);
57
- width: 80px; height: 80px; top: 50%; transform: translateY(-50%);
58
- border-radius: 50%;
59
- content: '»'; /* Use pseudo element content for icon */
60
- display: flex; align-items: center; justify-content: center;
61
- font-size: 30px; color: white;
62
- opacity: 1;
63
- animation: seek-pulse 0.4s ease-out;
64
- }
65
- .plyr__video-wrapper.seeking-backward::after {
66
- left: 15%; right: auto; transform: translateX(0);
67
- width: 80px; height: 80px; top: 50%; transform: translateY(-50%);
68
- border-radius: 50%;
69
- content: '«'; /* Use pseudo element content for icon */
70
- display: flex; align-items: center; justify-content: center;
71
- font-size: 30px; color: white;
72
- opacity: 1;
73
- animation: seek-pulse 0.4s ease-out;
74
- }
75
- @keyframes seek-pulse {
76
- 0% { transform: translateY(-50%) scale(0.8); opacity: 0.5; }
77
- 50% { transform: translateY(-50%) scale(1.1); opacity: 1; }
78
- 100% { transform: translateY(-50%) scale(1); opacity: 0; }
79
- }
80
-
81
  .no-videos { padding: 20px; text-align: center; color: #777; }
82
- @media (max-width: 768px) {
83
- .video-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
84
- .popup-content { width: 98%; max-width: none; height: auto; }
85
- #videoPlayer { max-height: 85vh; }
86
- .popup-title { font-size: 1em; }
87
- }
88
  </style>
89
  </head>
90
  <body>
91
- <div class="container">
 
92
  <div class="header">
93
- <h1>HF Video Library <small>(Plyr + Controls)</small></h1>
94
  </div>
95
  <div class="search-container">
96
  <input type="text" id="search" placeholder="Search videos...">
@@ -107,7 +68,6 @@
107
  <div class="popup-content">
108
  <button class="close-popup" id="closePopup">✕</button>
109
  <div class="popup-title" id="currentTitle">Video Title</div>
110
- <!-- Changed preload to "auto" -->
111
  <video id="videoPlayer" playsinline controls preload="auto">
112
  Your browser does not support the video tag.
113
  </video>
@@ -122,9 +82,9 @@
122
  let currentVideo = null;
123
  let currentPage = 1;
124
  const videosPerPage = 12;
125
- let player = null; // Plyr instance
126
- let lastTapTime = 0; // For double-tap detection
127
- let tapTimeout = null; // Timeout for distinguishing single/double taps
128
 
129
  const videoGrid = document.getElementById('videoGrid');
130
  const pagination = document.getElementById('pagination');
@@ -152,15 +112,16 @@
152
  }
153
  video.url = url.toString();
154
  } catch (e) {
155
- console.warn(`Invalid URL format for video "${video.title}": ${video.url}`, e);
156
  video.url = null; // Mark as invalid
157
  }
158
  }
159
  if (!video.title || !video.url) {
160
- console.warn("Skipping video with missing title or invalid URL:", video);
 
161
  }
162
  });
163
- videoData = videoData.filter(video => video.title && video.url);
164
  if (videoData.length === 0) {
165
  videoGrid.innerHTML = '<div class="no-videos">No valid videos found. Check database.json format and URLs.</div>';
166
  return;
@@ -181,7 +142,7 @@
181
  videoGrid.innerHTML = videoData.length > 0 && searchInput.value
182
  ? '<div class="no-videos">No videos match your search.</div>'
183
  : '<div class="no-videos">No videos available.</div>';
184
- renderPagination(); // Show pagination even if search yields no results (if there's data)
185
  return;
186
  }
187
  const startIndex = (currentPage - 1) * videosPerPage;
@@ -255,141 +216,186 @@
255
  });
256
  }
257
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
 
259
- // --- Updated Player Logic ---
 
 
 
 
 
 
 
 
 
 
 
260
 
261
  function playVideo(video) {
262
  if (!video || !video.url) {
263
- console.error("Cannot play video: Invalid video data provided.");
264
  return;
265
  }
 
 
 
 
 
266
  currentVideo = video;
267
  currentTitle.textContent = video.title;
268
  currentTitle.setAttribute('title', video.title);
 
 
 
269
  videoPlayerElement.src = video.url;
 
270
 
271
- if (player) {
272
- try { player.destroy(); } catch (e) { console.warn("Error destroying previous Plyr instance:", e); }
273
- player = null;
274
- }
275
 
 
276
  try {
 
277
  player = new Plyr(videoPlayerElement, {
278
  // Keep Plyr options
279
  title: video.title,
280
  controls: ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'],
281
  settings: ['captions', 'quality', 'speed', 'loop'],
282
- keyboard: { focused: true, global: false }, // Ensure keyboard works when focused
283
  tooltips: { controls: true, seek: true }
284
  });
 
 
285
 
286
- // --- Event Listeners for Focus and Double Tap ---
287
  player.on('ready', event => {
 
288
  const instance = event.detail.plyr;
289
  const container = instance.elements.container;
290
- // **Ensure Keyboard Focus**
291
- if (container) {
292
- container.setAttribute('tabindex', '-1'); // Make container focusable
293
- // Delay focus slightly to ensure popup transition is complete
294
- setTimeout(() => container.focus(), 100);
295
- }
296
 
297
- // **Setup Double Tap Listener**
298
- if (container) {
299
- container.addEventListener('touchstart', handleDoubleTap);
300
- // Add click listener as fallback for non-touch devices or testing
301
- container.addEventListener('click', handleDoubleTap);
 
 
 
 
 
 
302
  }
 
 
303
  });
304
 
305
  player.on('error', event => {
306
- console.error('Plyr error:', event.detail.plyr.source, event);
307
  alert(`Error playing video: ${video.title}. Check console.`);
308
- closePlayerPopup(); // Close popup on critical error
309
  });
310
 
311
- // Add listener to remove double-tap listener on destroy
312
  player.on('destroy', () => {
313
- const container = player?.elements?.container; // Use optional chaining
314
- if (container) {
315
- container.removeEventListener('touchstart', handleDoubleTap);
316
- container.removeEventListener('click', handleDoubleTap);
317
- }
318
- clearTimeout(tapTimeout); // Clear any pending timeouts
319
  });
320
 
321
- playerPopup.classList.add('active');
 
 
322
  document.body.style.overflow = 'hidden';
 
323
 
324
  } catch (error) {
325
  console.error("Failed to initialize Plyr:", error);
326
  alert(`Error initializing video player for "${video.title}". Check console.`);
327
- // Simple fallback: just show the native controls if Plyr fails
328
- videoPlayerElement.controls = true;
329
  playerPopup.classList.add('active');
330
  document.body.style.overflow = 'hidden';
331
  }
332
  }
333
 
334
- // --- Double Tap Handler ---
335
  function handleDoubleTap(e) {
336
- if (!player || !player.elements.container) return;
337
-
338
- // Ignore taps on controls
 
339
  if (e.target.closest('.plyr__controls')) {
 
340
  return;
341
  }
342
 
343
- const TappedElement = player.elements.container; // Listen on the main container
344
  const now = new Date().getTime();
345
- const DOUBLE_TAP_DELAY = 350; // Milliseconds delay for double tap
346
 
347
  if (now - lastTapTime < DOUBLE_TAP_DELAY) {
348
- // Double tap detected
349
- clearTimeout(tapTimeout); // Prevent single tap action
350
- lastTapTime = 0; // Reset tap time
351
 
352
  const rect = TappedElement.getBoundingClientRect();
353
- // Use touch coordinate if available, otherwise click coordinate
354
  const clientX = e.touches ? e.touches[0].clientX : e.clientX;
355
  const tapX = clientX - rect.left;
356
  const width = rect.width;
 
357
 
358
- // Optional: Add visual feedback class
359
- const wrapper = TappedElement.querySelector('.plyr__video-wrapper');
360
-
361
- if (tapX < width * 0.35) { // Left ~1/3
362
  player.rewind(10);
363
  if (wrapper) flashSeekIndicator(wrapper, 'backward');
364
- } else if (tapX > width * 0.65) { // Right ~1/3
 
365
  player.forward(10);
366
  if (wrapper) flashSeekIndicator(wrapper, 'forward');
367
- } else { // Middle
 
368
  player.togglePlay();
369
- }
370
- e.preventDefault(); // Prevent potential browser zoom or other default actions
371
 
372
  } else {
373
- // First tap (or tap after long delay)
374
  lastTapTime = now;
375
- // We don't need a timeout here because Plyr's default single-click
376
- // behaviour (toggle play/pause) is usually desired. If a double-tap
377
- // happens quickly, the `clearTimeout` and `lastTapTime = 0` above
378
- // will effectively cancel the single-tap interpretation.
379
  }
380
  }
381
-
382
- // Helper for visual feedback (optional)
383
  function flashSeekIndicator(element, direction) {
384
  element.classList.remove('seeking-forward', 'seeking-backward');
385
- // Force reflow hack to restart animation
386
  void element.offsetWidth;
387
- if (direction === 'forward') {
388
- element.classList.add('seeking-forward');
389
- } else if (direction === 'backward') {
390
- element.classList.add('seeking-backward');
391
- }
392
- // Remove class after animation (match animation duration)
393
  setTimeout(() => {
394
  element.classList.remove('seeking-forward', 'seeking-backward');
395
  }, 400);
@@ -397,57 +403,23 @@
397
 
398
 
399
  function closePlayerPopup() {
400
- if (player) {
401
- try {
402
- // Remove listeners before destroying
403
- const container = player.elements.container;
404
- if (container) {
405
- container.removeEventListener('touchstart', handleDoubleTap);
406
- container.removeEventListener('click', handleDoubleTap);
407
- }
408
- player.destroy(); // This should pause and clean up
409
- } catch (e) { console.warn("Error destroying Plyr instance on close:", e); }
410
- player = null;
411
- }
412
- // Fallback cleanup
413
- videoPlayerElement.pause();
414
- videoPlayerElement.removeAttribute('src');
415
- videoPlayerElement.load();
416
- clearTimeout(tapTimeout); // Clear tap timeout
417
 
418
  playerPopup.classList.remove('active');
419
  document.body.style.overflow = '';
420
- currentVideo = null;
421
- lastTapTime = 0; // Reset tap time
422
  }
423
 
424
 
425
- // --- Event Listeners (Search, Popup Close, Escape Key) ---
426
- searchInput.addEventListener('input', function() {
427
- const searchTerm = this.value.toLowerCase().trim();
428
- filteredVideos = videoData.filter(video =>
429
- video.title.toLowerCase().includes(searchTerm)
430
- );
431
- currentPage = 1;
432
- renderPagination();
433
- renderVideoGrid();
434
- });
435
-
436
  closePopup.addEventListener('click', closePlayerPopup);
 
 
437
 
438
- playerPopup.addEventListener('click', function(e) {
439
- if (e.target === playerPopup) {
440
- closePlayerPopup();
441
- }
442
- });
443
-
444
- // Use Plyr's keyboard handling, but keep Escape as a global close fallback
445
- document.addEventListener('keydown', function(e) {
446
- if (e.key === 'Escape' && playerPopup.classList.contains('active')) {
447
- closePlayerPopup();
448
- }
449
- // Let Plyr handle Space, Arrows, F, M when it has focus
450
- });
451
 
452
  // --- Initial Load ---
453
  fetchVideoData();
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>HF Video Library (Plyr + Controls - Fixed Cleanup)</title>
7
 
8
  <!-- Plyr CSS -->
9
  <link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css" />
10
 
11
  <style>
12
+ /* --- Keep ALL your previous styles --- */
13
+ /* --- Keep your existing base styles --- */
14
  * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
15
  body { background-color: #f5f5f5; padding: 20px; max-width: 1200px; margin: 0 auto; }
16
  .container { display: flex; flex-direction: column; gap: 20px; }
 
38
  .close-popup:hover { background-color: rgba(50,50,50,0.9); }
39
  .popup-title { color: white; padding: 12px 15px; font-size: 1.1em; background-color: #111; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; order: -1; }
40
  #videoPlayer { display: block; width: 100%; height: auto; max-height: 80vh; background-color: #000; }
 
41
  :root { --plyr-color-main: #007bff; }
42
+ .plyr__video-wrapper::after { content: ''; position: absolute; top: 0; left: 50%; transform: translateX(-50%); width: 0; height: 100%; background: rgba(255, 255, 255, 0.15); border-radius: 50%; opacity: 0; transition: width 0.2s ease-out, opacity 0.2s ease-out; pointer-events: none; }
43
+ .plyr__video-wrapper.seeking-forward::after { left: auto; right: 15%; transform: translateX(0); width: 80px; height: 80px; top: 50%; transform: translateY(-50%); border-radius: 50%; content: '»'; display: flex; align-items: center; justify-content: center; font-size: 30px; color: white; opacity: 1; animation: seek-pulse 0.4s ease-out; }
44
+ .plyr__video-wrapper.seeking-backward::after { left: 15%; right: auto; transform: translateX(0); width: 80px; height: 80px; top: 50%; transform: translateY(-50%); border-radius: 50%; content: '«'; display: flex; align-items: center; justify-content: center; font-size: 30px; color: white; opacity: 1; animation: seek-pulse 0.4s ease-out; }
45
+ @keyframes seek-pulse { 0% { transform: translateY(-50%) scale(0.8); opacity: 0.5; } 50% { transform: translateY(-50%) scale(1.1); opacity: 1; } 100% { transform: translateY(-50%) scale(1); opacity: 0; } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  .no-videos { padding: 20px; text-align: center; color: #777; }
47
+ @media (max-width: 768px) { .video-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); } .popup-content { width: 98%; max-width: none; height: auto; } #videoPlayer { max-height: 85vh; } .popup-title { font-size: 1em; } }
 
 
 
 
 
48
  </style>
49
  </head>
50
  <body>
51
+ <!-- Keep Body HTML structure as before -->
52
+ <div class="container">
53
  <div class="header">
54
+ <h1>HF Video Library <small>(Plyr + Controls - Fixed Cleanup)</small></h1>
55
  </div>
56
  <div class="search-container">
57
  <input type="text" id="search" placeholder="Search videos...">
 
68
  <div class="popup-content">
69
  <button class="close-popup" id="closePopup">✕</button>
70
  <div class="popup-title" id="currentTitle">Video Title</div>
 
71
  <video id="videoPlayer" playsinline controls preload="auto">
72
  Your browser does not support the video tag.
73
  </video>
 
82
  let currentVideo = null;
83
  let currentPage = 1;
84
  const videosPerPage = 12;
85
+ let player = null;
86
+ let lastTapTime = 0;
87
+ let tapTimeout = null;
88
 
89
  const videoGrid = document.getElementById('videoGrid');
90
  const pagination = document.getElementById('pagination');
 
112
  }
113
  video.url = url.toString();
114
  } catch (e) {
115
+ console.warn(`Invalid URL format for video "${video.title || 'Untitled'}": ${video.url}`, e);
116
  video.url = null; // Mark as invalid
117
  }
118
  }
119
  if (!video.title || !video.url) {
120
+ // Ensure title exists even if empty for logging
121
+ console.warn(`Skipping video with missing title or invalid/missing URL:`, video);
122
  }
123
  });
124
+ videoData = videoData.filter(video => video.title && video.url); // Ensure only valid entries proceed
125
  if (videoData.length === 0) {
126
  videoGrid.innerHTML = '<div class="no-videos">No valid videos found. Check database.json format and URLs.</div>';
127
  return;
 
142
  videoGrid.innerHTML = videoData.length > 0 && searchInput.value
143
  ? '<div class="no-videos">No videos match your search.</div>'
144
  : '<div class="no-videos">No videos available.</div>';
145
+ renderPagination();
146
  return;
147
  }
148
  const startIndex = (currentPage - 1) * videosPerPage;
 
216
  });
217
  }
218
 
219
+ // --- Player Logic with Enhanced Cleanup ---
220
+
221
+ /**
222
+ * Cleans up the existing player instance and resets the video element.
223
+ * @param {string} context - Identifier for logging (e.g., 'closePlayerPopup', 'playVideoStart')
224
+ */
225
+ function cleanupPlayer(context = 'unknown') {
226
+ console.log(`Cleanup requested from: ${context}`);
227
+ if (player) {
228
+ console.log("Player instance found, attempting cleanup.");
229
+ try {
230
+ const container = player.elements?.container; // Use optional chaining
231
+ if (container) {
232
+ console.log("Removing double-tap listeners.");
233
+ container.removeEventListener('touchstart', handleDoubleTap);
234
+ container.removeEventListener('click', handleDoubleTap);
235
+ } else {
236
+ console.log("Player container not found, skipping listener removal.");
237
+ }
238
+
239
+ console.log("Destroying Plyr instance.");
240
+ player.destroy();
241
+
242
+ } catch (e) {
243
+ console.warn(`Error destroying Plyr instance during cleanup (${context}):`, e);
244
+ } finally {
245
+ player = null; // Ensure player is nullified
246
+ console.log("Plyr instance variable nullified.");
247
+ }
248
+ } else {
249
+ console.log("No active player instance found to clean up.");
250
+ }
251
 
252
+ // --- Force reset the native video element ---
253
+ // This is crucial to prevent lingering state/playback
254
+ console.log("Resetting native video element.");
255
+ videoPlayerElement.pause();
256
+ videoPlayerElement.removeAttribute('src'); // Remove the source attribute
257
+ videoPlayerElement.src = ""; // Explicitly set src to empty string
258
+ videoPlayerElement.load(); // Force the browser to unload the media
259
+
260
+ clearTimeout(tapTimeout); // Clear any pending tap timeouts
261
+ lastTapTime = 0; // Reset tap timer
262
+ console.log("Native element reset complete.");
263
+ }
264
 
265
  function playVideo(video) {
266
  if (!video || !video.url) {
267
+ console.error("Cannot play video: Invalid video data provided.", video);
268
  return;
269
  }
270
+ console.log(`Attempting to play video: ${video.title} (${video.url})`);
271
+
272
+ // ***** 1. Force Cleanup Before Starting *****
273
+ cleanupPlayer('playVideoStart'); // Clean up any previous instance thoroughly
274
+
275
  currentVideo = video;
276
  currentTitle.textContent = video.title;
277
  currentTitle.setAttribute('title', video.title);
278
+
279
+ // ***** 2. Set New Source *****
280
+ // Note: cleanupPlayer already reset src, so setting it here is correct
281
  videoPlayerElement.src = video.url;
282
+ console.log("Set new video source.");
283
 
 
 
 
 
284
 
285
+ // ***** 3. Initialize New Plyr Instance *****
286
  try {
287
+ console.log("Initializing new Plyr instance.");
288
  player = new Plyr(videoPlayerElement, {
289
  // Keep Plyr options
290
  title: video.title,
291
  controls: ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'],
292
  settings: ['captions', 'quality', 'speed', 'loop'],
293
+ keyboard: { focused: true, global: false },
294
  tooltips: { controls: true, seek: true }
295
  });
296
+ console.log("Plyr instance created.");
297
+
298
 
299
+ // --- Setup Event Listeners for the NEW Instance ---
300
  player.on('ready', event => {
301
+ console.log("Plyr ready event fired.");
302
  const instance = event.detail.plyr;
303
  const container = instance.elements.container;
 
 
 
 
 
 
304
 
305
+ if (container) {
306
+ console.log("Setting up focus and double-tap listeners.");
307
+ container.setAttribute('tabindex', '-1');
308
+ setTimeout(() => {
309
+ try { container.focus(); } catch(e) { console.warn("Failed to focus player container", e); }
310
+ }, 100); // Delay focus
311
+
312
+ container.addEventListener('touchstart', handleDoubleTap, { passive: false }); // Use passive: false if preventDefault is needed
313
+ container.addEventListener('click', handleDoubleTap);
314
+ } else {
315
+ console.warn("Plyr container not found on ready.");
316
  }
317
+ // Optional: Autoplay (often blocked)
318
+ // instance.play().catch(e => console.log("Autoplay prevented/failed:", e));
319
  });
320
 
321
  player.on('error', event => {
322
+ console.error('Plyr playback error:', event.detail.plyr.source, event);
323
  alert(`Error playing video: ${video.title}. Check console.`);
324
+ closePlayerPopup(); // Close on critical error
325
  });
326
 
327
+ // Listener to aid cleanup on destroy (redundant with our explicit cleanup, but good practice)
328
  player.on('destroy', () => {
329
+ console.log("Plyr destroy event fired.");
330
+ // No need to remove listeners here, cleanupPlayer handles it.
 
 
 
 
331
  });
332
 
333
+
334
+ // ***** 4. Show Popup *****
335
+ playerPopup.classList.add('active');
336
  document.body.style.overflow = 'hidden';
337
+ console.log("Player popup activated.");
338
 
339
  } catch (error) {
340
  console.error("Failed to initialize Plyr:", error);
341
  alert(`Error initializing video player for "${video.title}". Check console.`);
342
+ // Basic fallback if Plyr fails
343
+ videoPlayerElement.controls = true; // Show native controls
344
  playerPopup.classList.add('active');
345
  document.body.style.overflow = 'hidden';
346
  }
347
  }
348
 
349
+ // --- Double Tap Handler (Keep as before) ---
350
  function handleDoubleTap(e) {
351
+ if (!player || !player.elements.container || !player.elements.wrapper) {
352
+ // console.log("Double tap ignored: Player or elements not ready.");
353
+ return;
354
+ }
355
  if (e.target.closest('.plyr__controls')) {
356
+ // console.log("Double tap ignored: Tap on controls.");
357
  return;
358
  }
359
 
360
+ const TappedElement = player.elements.container;
361
  const now = new Date().getTime();
362
+ const DOUBLE_TAP_DELAY = 350;
363
 
364
  if (now - lastTapTime < DOUBLE_TAP_DELAY) {
365
+ console.log("Double tap detected.");
366
+ clearTimeout(tapTimeout);
367
+ lastTapTime = 0;
368
 
369
  const rect = TappedElement.getBoundingClientRect();
 
370
  const clientX = e.touches ? e.touches[0].clientX : e.clientX;
371
  const tapX = clientX - rect.left;
372
  const width = rect.width;
373
+ const wrapper = player.elements.wrapper; // wrapper for visual feedback
374
 
375
+ if (tapX < width * 0.35) {
376
+ console.log("Double tap: Rewind 10s");
 
 
377
  player.rewind(10);
378
  if (wrapper) flashSeekIndicator(wrapper, 'backward');
379
+ } else if (tapX > width * 0.65) {
380
+ console.log("Double tap: Forward 10s");
381
  player.forward(10);
382
  if (wrapper) flashSeekIndicator(wrapper, 'forward');
383
+ } else {
384
+ console.log("Double tap: Toggle Play/Pause");
385
  player.togglePlay();
386
+ }
387
+ e.preventDefault(); // Crucial for touchstart
388
 
389
  } else {
390
+ // console.log("Single tap registered.");
391
  lastTapTime = now;
392
+ // No timeout needed here, let default single click handle play/pause toggle
 
 
 
393
  }
394
  }
 
 
395
  function flashSeekIndicator(element, direction) {
396
  element.classList.remove('seeking-forward', 'seeking-backward');
 
397
  void element.offsetWidth;
398
+ element.classList.add(direction === 'forward' ? 'seeking-forward' : 'seeking-backward');
 
 
 
 
 
399
  setTimeout(() => {
400
  element.classList.remove('seeking-forward', 'seeking-backward');
401
  }, 400);
 
403
 
404
 
405
  function closePlayerPopup() {
406
+ console.log("Close popup requested.");
407
+ // ***** Call the centralized cleanup function *****
408
+ cleanupPlayer('closePlayerPopup');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
 
410
  playerPopup.classList.remove('active');
411
  document.body.style.overflow = '';
412
+ currentVideo = null; // Clear current video reference
413
+ console.log("Popup closed and state reset.");
414
  }
415
 
416
 
417
+ // --- Event Listeners (Search, Popup Close, Escape Key - Keep as before) ---
418
+ searchInput.addEventListener('input', function() { /* ... */ });
 
 
 
 
 
 
 
 
 
419
  closePopup.addEventListener('click', closePlayerPopup);
420
+ playerPopup.addEventListener('click', function(e) { if (e.target === playerPopup) { closePlayerPopup(); } });
421
+ document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && playerPopup.classList.contains('active')) { closePlayerPopup(); } });
422
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
 
424
  // --- Initial Load ---
425
  fetchVideoData();