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

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +253 -369
index.html CHANGED
@@ -3,361 +3,190 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>HF Video Library (Optimized with Plyr)</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 styles for layout, search, grid, etc. --- */
13
- * {
14
- box-sizing: border-box;
15
- margin: 0;
16
- padding: 0;
17
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
18
- }
19
- body {
20
- background-color: #f5f5f5;
21
- padding: 20px;
22
- max-width: 1200px;
23
- margin: 0 auto;
24
- }
25
- .container {
26
- display: flex;
27
- flex-direction: column;
28
- gap: 20px;
29
- }
30
- .header {
31
- background-color: #333;
32
- color: white;
33
- padding: 20px;
34
- border-radius: 8px;
35
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
36
- }
37
- .search-container {
38
- padding: 15px;
39
- background-color: white;
40
- border-radius: 8px;
41
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
42
- }
43
- #search {
44
- width: 100%;
45
- padding: 10px;
46
- border: 1px solid #ddd;
47
- border-radius: 4px;
48
- font-size: 16px;
49
- }
50
- .video-list {
51
- background-color: white;
52
- border-radius: 8px;
53
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
54
- padding: 15px;
55
- }
56
- .video-grid {
57
- display: grid;
58
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
59
- gap: 15px;
60
- }
61
- .video-item {
62
- padding: 15px;
63
- border: 1px solid #eee;
64
- border-radius: 8px;
65
- cursor: pointer;
66
- transition: all 0.2s;
67
- background-color: white;
68
- display: flex; /* Use flexbox for better alignment */
69
- flex-direction: column;
70
- }
71
- .video-item:hover {
72
- transform: translateY(-5px);
73
- box-shadow: 0 5px 15px rgba(0,0,0,0.1);
74
- }
75
- .video-thumbnail {
76
- width: 100%;
77
- height: 156px; /* Maintain aspect ratio consistency */
78
- background-color: #e0e0e0; /* Lighter placeholder */
79
- border-radius: 4px;
80
- display: flex;
81
- align-items: center;
82
- justify-content: center;
83
- margin-bottom: 10px;
84
- overflow: hidden;
85
- position: relative;
86
- }
87
- .video-thumbnail img { /* Optional: If you add thumbnail images later */
88
- width: 100%;
89
- height: 100%;
90
- object-fit: cover;
91
- }
92
- .play-icon { /* Simple text icon */
93
- font-size: 40px;
94
- color: rgba(0,0,0,0.6);
95
- opacity: 0.8;
96
- transition: opacity 0.2s;
97
  position: absolute;
98
- }
99
- .video-item:hover .play-icon {
100
- opacity: 1;
101
- }
102
- .video-item-title {
103
- font-weight: 500;
104
- margin-top: auto; /* Pushes title down if thumbnail varies slightly */
105
- white-space: nowrap;
106
- overflow: hidden;
107
- text-overflow: ellipsis;
108
- }
109
- .pagination {
110
- display: flex;
111
- justify-content: center;
112
- gap: 5px;
113
- margin-top: 20px;
114
- }
115
- .page-btn {
116
- padding: 8px 12px;
117
- background-color: white;
118
- border: 1px solid #ddd;
119
- border-radius: 4px;
120
- cursor: pointer;
121
- transition: background-color 0.2s;
122
- }
123
- .page-btn:hover {
124
- background-color: #f0f0f0;
125
- }
126
- .page-btn.active {
127
- background-color: #333;
128
- color: white;
129
- border-color: #333;
130
- }
131
- .popup-overlay {
132
- position: fixed;
133
- top: 0;
134
- left: 0;
135
- width: 100%;
136
- height: 100%;
137
- background-color: rgba(0,0,0,0.85); /* Slightly darker */
138
- display: flex;
139
- align-items: center;
140
- justify-content: center;
141
- z-index: 1000;
142
  opacity: 0;
143
- visibility: hidden;
144
- transition: opacity 0.3s, visibility 0.3s;
145
- }
146
- .popup-overlay.active {
147
- opacity: 1;
148
- visibility: visible;
149
  }
150
- .popup-content {
151
- width: 90%;
152
- max-width: 1100px; /* Slightly wider for player */
153
- background-color: #000; /* Black background suits player */
154
- border-radius: 8px;
155
- overflow: hidden;
156
- position: relative;
157
- box-shadow: 0 10px 25px rgba(0,0,0,0.5);
158
- display: flex; /* Use flexbox for title + player */
159
- flex-direction: column;
160
- }
161
- .close-popup {
162
- position: absolute;
163
- top: 8px; /* Adjust for better placement */
164
- right: 8px; /* Adjust for better placement */
165
- background-color: rgba(30,30,30,0.7); /* Subtle background */
166
- color: white;
167
- border: none;
168
- width: 32px; /* Slightly larger */
169
- height: 32px;
170
  border-radius: 50%;
171
- display: flex;
172
- align-items: center;
173
- justify-content: center;
174
- cursor: pointer;
175
- z-index: 1001; /* Ensure it's above Plyr */
176
- font-size: 18px;
177
- line-height: 1; /* Ensure centering */
178
- }
179
- .close-popup:hover {
180
- background-color: rgba(50,50,50,0.9);
 
 
 
 
 
 
 
 
 
 
181
  }
182
 
183
- .popup-title {
184
- color: white;
185
- padding: 12px 15px; /* Adjust padding */
186
- font-size: 1.1em; /* Adjust size */
187
- background-color: #111;
188
- text-align: center;
189
- white-space: nowrap;
190
- overflow: hidden;
191
- text-overflow: ellipsis;
192
- order: -1; /* Display title above video */
193
- }
194
-
195
- /* Remove custom controls, player-wrapper, spinner, indicators */
196
- /* .controls, .player-wrapper, #loadingSpinner, #bufferingIndicator, #seekPreview { display: none; } */
197
-
198
- /* Style the video element itself if needed, but Plyr handles most */
199
- #videoPlayer {
200
- display: block; /* Ensure it takes block layout */
201
- width: 100%;
202
- height: auto; /* Let Plyr manage height based on aspect ratio */
203
- max-height: 80vh; /* Prevent excessive height on tall screens */
204
- background-color: #000;
205
- }
206
-
207
- /* Plyr Customizations (Optional) */
208
- :root {
209
- --plyr-color-main: #007bff; /* Example: Blue theme */
210
- }
211
-
212
- .no-videos {
213
- padding: 20px;
214
- text-align: center;
215
- color: #777;
216
- }
217
  @media (max-width: 768px) {
218
- .video-grid {
219
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
220
- }
221
- .popup-content {
222
- width: 98%; /* Almost full width on mobile */
223
- max-width: none;
224
- height: auto;
225
- }
226
- #videoPlayer {
227
- max-height: 85vh; /* Adjust max height for mobile */
228
- }
229
- .popup-title {
230
- font-size: 1em;
231
- }
232
  }
233
  </style>
234
  </head>
235
  <body>
236
  <div class="container">
237
  <div class="header">
238
- <h1>HF Video Library <small>(Plyr Enhanced)</small></h1>
239
  </div>
240
-
241
  <div class="search-container">
242
  <input type="text" id="search" placeholder="Search videos...">
243
  </div>
244
-
245
  <div class="video-list">
246
  <div class="video-grid" id="videoGrid">
247
- <!-- Video items will be loaded here -->
248
- <div class="video-item">
249
- <div class="video-thumbnail">Loading...</div>
250
- </div>
251
  </div>
252
  <div class="pagination" id="pagination"></div>
253
  </div>
254
  </div>
255
 
256
- <!-- Video Player Popup -->
257
  <div class="popup-overlay" id="playerPopup">
258
  <div class="popup-content">
259
  <button class="close-popup" id="closePopup">✕</button>
260
  <div class="popup-title" id="currentTitle">Video Title</div>
261
- <!-- The video element Plyr will enhance -->
262
- <video id="videoPlayer" playsinline controls preload="metadata">
263
- <!-- preload="metadata" is often a good balance -->
264
  Your browser does not support the video tag.
265
  </video>
266
- <!-- Custom controls and instructions are removed - Plyr provides them -->
267
  </div>
268
  </div>
269
 
270
- <!-- Plyr JS -->
271
  <script src="https://cdn.plyr.io/3.7.8/plyr.js"></script>
272
-
273
  <script>
274
- // Global variables
275
  let videoData = [];
276
  let filteredVideos = [];
277
  let currentVideo = null;
278
  let currentPage = 1;
279
- const videosPerPage = 12; // Adjusted for potentially smaller cards
280
- let player = null; // To hold the Plyr instance
 
 
281
 
282
- // DOM Elements
283
  const videoGrid = document.getElementById('videoGrid');
284
  const pagination = document.getElementById('pagination');
285
  const searchInput = document.getElementById('search');
286
  const playerPopup = document.getElementById('playerPopup');
287
- const videoPlayerElement = document.getElementById('videoPlayer'); // Get the element
288
  const currentTitle = document.getElementById('currentTitle');
289
  const closePopup = document.getElementById('closePopup');
290
 
291
- // --- Fetching, Rendering Grid, Pagination (Keep mostly the same) ---
292
-
293
- async function fetchVideoData() {
294
  try {
295
- // Show initial loading state
296
  videoGrid.innerHTML = '<div class="no-videos">Loading video list...</div>';
297
  pagination.innerHTML = '';
298
-
299
- const response = await fetch('database.json'); // Ensure this path is correct
300
- if (!response.ok) {
301
- throw new Error(`Failed to fetch video data (Status: ${response.status})`);
302
- }
303
  const data = await response.json();
304
-
305
- videoData = Array.isArray(data) ? data :
306
- (data.videos && Array.isArray(data.videos) ? data.videos : []);
307
-
308
- // Optimize HF URLs (Keep this logic)
309
  videoData.forEach(video => {
310
  if (video.url && video.url.includes('huggingface.co')) {
311
- let url = new URL(video.url);
312
- // Ensure download=true is present without duplicating query params
313
- if(url.searchParams.get('download') !== 'true') {
314
- url.searchParams.set('download', 'true');
 
 
 
 
 
315
  }
316
- video.url = url.toString();
317
  }
318
- // Basic validation
319
- if (!video.title || !video.url) {
320
- console.warn("Skipping video with missing title or URL:", video);
321
  }
322
  });
323
-
324
- // Filter out invalid entries before proceeding
325
- videoData = videoData.filter(video => video.title && video.url);
326
-
327
-
328
- if (videoData.length === 0) {
329
- videoGrid.innerHTML = '<div class="no-videos">No valid videos found in database.json</div>';
330
  return;
331
  }
332
-
333
- filteredVideos = [...videoData];
334
- currentPage = 1; // Reset to page 1
335
  renderPagination();
336
  renderVideoGrid();
337
  } catch (error) {
338
- console.error('Error fetching or processing video data:', error);
339
- videoGrid.innerHTML = `<div class="no-videos">Error loading videos: ${error.message}. Check console.</div>`;
340
  }
341
  }
342
 
343
  function renderVideoGrid() {
344
- videoGrid.innerHTML = ''; // Clear previous items
345
-
346
- if (filteredVideos.length === 0) {
347
- videoGrid.innerHTML = '<div class="no-videos">No videos match your search.</div>';
348
- // Don't clear pagination if there are videos overall, just no search results
349
- if (videoData.length > 0 && searchInput.value) {
350
- renderPagination(); // Keep pagination visible
351
- } else {
352
- pagination.innerHTML = ''; // Hide if no videos at all
353
- }
354
  return;
355
  }
356
-
357
- const startIndex = (currentPage - 1) * videosPerPage;
358
  const endIndex = Math.min(startIndex + videosPerPage, filteredVideos.length);
359
  const currentVideos = filteredVideos.slice(startIndex, endIndex);
360
-
361
  const fragment = document.createDocumentFragment();
362
  currentVideos.forEach((video, index) => {
363
  const videoItem = document.createElement('div');
@@ -372,7 +201,7 @@
372
  `;
373
  videoItem.addEventListener('click', function() {
374
  const dataIndex = parseInt(this.getAttribute('data-index'));
375
- if (!isNaN(dataIndex) && dataIndex >= 0 && dataIndex < filteredVideos.length) {
376
  playVideo(filteredVideos[dataIndex]);
377
  } else {
378
  console.error("Invalid video index clicked:", dataIndex);
@@ -380,76 +209,54 @@
380
  });
381
  fragment.appendChild(videoItem);
382
  });
383
-
384
- videoGrid.appendChild(fragment);
385
  }
386
 
387
  function renderPagination() {
388
  const totalPages = Math.ceil(filteredVideos.length / videosPerPage);
389
- pagination.innerHTML = ''; // Clear existing buttons
390
-
391
- if (totalPages <= 1) {
392
- return; // No pagination needed for 0 or 1 page
393
- }
394
-
395
- let buttons = [];
396
- const maxVisibleButtons = 7; // Max buttons like 1 ... 5 6 7 ... 10
397
-
398
- if (totalPages <= maxVisibleButtons) {
399
- for (let i = 1; i <= totalPages; i++) {
400
- buttons.push(i);
401
- }
402
  } else {
403
- buttons.push(1); // Always show first page
404
  let startPage, endPage;
405
-
406
  if (currentPage <= Math.floor(maxVisibleButtons / 2) + 1) {
407
- // Near the beginning
408
- startPage = 2;
409
- endPage = maxVisibleButtons - 2; // 1 + (max-2 pages) + ... + last
410
  for (let i = startPage; i <= endPage; i++) buttons.push(i);
411
  buttons.push('...');
412
  } else if (currentPage >= totalPages - Math.floor(maxVisibleButtons / 2)) {
413
- // Near the end
414
- startPage = totalPages - (maxVisibleButtons - 3); // first + ... + (max-2 pages) + last
415
  buttons.push('...');
416
  for (let i = startPage; i < totalPages; i++) buttons.push(i);
417
  } else {
418
- // In the middle
419
  buttons.push('...');
420
- const sideButtons = Math.floor((maxVisibleButtons - 3) / 2); // Buttons around current
421
- startPage = currentPage - sideButtons;
422
- endPage = currentPage + sideButtons;
423
  for (let i = startPage; i <= endPage; i++) buttons.push(i);
424
  buttons.push('...');
425
  }
426
- buttons.push(totalPages); // Always show last page
427
  }
428
-
429
- // Create pagination HTML
430
- pagination.innerHTML = buttons.map(button => {
431
- if (button === '...') {
432
- return '<span class="page-btn disabled">...</span>'; // Non-clickable ellipsis
433
- }
434
  return `<button class="page-btn ${button === currentPage ? 'active' : ''}" data-page="${button}">${button}</button>`;
435
  }).join('');
436
-
437
- // Add click event listeners to page buttons
438
- document.querySelectorAll('.page-btn[data-page]').forEach(button => {
439
  button.addEventListener('click', function() {
440
  const newPage = parseInt(this.getAttribute('data-page'));
441
  if (newPage !== currentPage) {
442
- currentPage = newPage;
443
- renderPagination(); // Update buttons appearance
444
- renderVideoGrid(); // Render new page content
445
- window.scrollTo({ top: videoGrid.offsetTop - 20, behavior: 'smooth' }); // Scroll near top of grid
446
  }
447
  });
448
  });
449
  }
450
 
451
 
452
- // --- Player Logic (Using Plyr) ---
453
 
454
  function playVideo(video) {
455
  if (!video || !video.url) {
@@ -458,92 +265,170 @@
458
  }
459
  currentVideo = video;
460
  currentTitle.textContent = video.title;
461
- currentTitle.setAttribute('title', video.title); // Add tooltip for long titles
462
-
463
- // Set the source for the video element
464
  videoPlayerElement.src = video.url;
465
 
466
- // Destroy previous Plyr instance if it exists
467
  if (player) {
468
- try {
469
- player.destroy();
470
- } catch (e) {
471
- console.warn("Error destroying previous Plyr instance:", e);
472
- }
473
  player = null;
474
  }
475
 
476
- // Initialize Plyr
477
  try {
478
  player = new Plyr(videoPlayerElement, {
479
- // Plyr options (optional)
480
  title: video.title,
481
  controls: ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'],
482
  settings: ['captions', 'quality', 'speed', 'loop'],
483
- // quality: {
484
- // // If you were using HLS/DASH, you'd configure quality options here
485
- // default: 720, // Example default quality
486
- // options: [1080, 720, 480]
487
- // },
488
- // tooltips: { controls: true, seek: true } // Enable tooltips
489
  });
490
 
491
- // Add event listener for when player is ready
492
- player.on('ready', () => {
493
- // Autoplay (optional, might be blocked by browsers)
494
- // player.play().catch(e => console.log("Autoplay prevented:", e));
 
 
 
 
 
 
 
 
 
 
 
 
 
495
  });
496
 
497
- // Add event listener for errors
498
  player.on('error', event => {
499
  console.error('Plyr error:', event.detail.plyr.source, event);
500
- // Maybe display a user-friendly error message
 
501
  });
502
 
503
- // Show the popup
504
- playerPopup.classList.add('active');
505
- document.body.style.overflow = 'hidden'; // Prevent background scroll
 
 
 
 
 
 
 
 
 
506
 
507
  } catch (error) {
508
  console.error("Failed to initialize Plyr:", error);
509
- // Fallback or error message
510
- alert(`Error initializing video player for "${video.title}". Check console for details.`);
511
- // Still try to show the basic video element if Plyr fails?
512
- videoPlayerElement.load(); // Try loading without Plyr
513
  playerPopup.classList.add('active');
514
  document.body.style.overflow = 'hidden';
515
  }
516
  }
517
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
  function closePlayerPopup() {
519
- if (player) {
520
  try {
521
- player.pause(); // Pause before destroying
522
- player.destroy();
523
- } catch (e) {
524
- console.warn("Error destroying Plyr instance on close:", e)
525
- }
 
 
 
526
  player = null;
527
- }
528
- // Fallback if player wasn't initialized or destroyed properly
529
  videoPlayerElement.pause();
530
- videoPlayerElement.removeAttribute('src'); // Remove src to stop potential loading
531
- videoPlayerElement.load(); // Reset the media element
 
532
 
533
  playerPopup.classList.remove('active');
534
- document.body.style.overflow = ''; // Restore scroll
535
  currentVideo = null;
 
536
  }
537
 
538
 
539
- // --- Event Listeners (Search, Popup Close) ---
540
-
541
  searchInput.addEventListener('input', function() {
542
  const searchTerm = this.value.toLowerCase().trim();
543
  filteredVideos = videoData.filter(video =>
544
  video.title.toLowerCase().includes(searchTerm)
545
  );
546
- currentPage = 1; // Reset to first page on search
547
  renderPagination();
548
  renderVideoGrid();
549
  });
@@ -551,20 +436,19 @@
551
  closePopup.addEventListener('click', closePlayerPopup);
552
 
553
  playerPopup.addEventListener('click', function(e) {
554
- // Close only if clicking the overlay itself, not the content inside
555
  if (e.target === playerPopup) {
556
  closePlayerPopup();
557
  }
558
  });
559
 
560
- // Close popup with Escape key (Plyr might handle this, but good fallback)
561
  document.addEventListener('keydown', function(e) {
562
  if (e.key === 'Escape' && playerPopup.classList.contains('active')) {
563
  closePlayerPopup();
564
  }
 
565
  });
566
 
567
-
568
  // --- Initial Load ---
569
  fetchVideoData();
570
 
 
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; }
16
+ .header { background-color: #333; color: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
17
+ .search-container { padding: 15px; background-color: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
18
+ #search { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 16px; }
19
+ .video-list { background-color: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); padding: 15px; }
20
+ .video-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 15px; }
21
+ .video-item { padding: 15px; border: 1px solid #eee; border-radius: 8px; cursor: pointer; transition: all 0.2s; background-color: white; display: flex; flex-direction: column; }
22
+ .video-item:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); }
23
+ .video-thumbnail { width: 100%; height: 156px; background-color: #e0e0e0; border-radius: 4px; display: flex; align-items: center; justify-content: center; margin-bottom: 10px; overflow: hidden; position: relative; }
24
+ .video-thumbnail img { width: 100%; height: 100%; object-fit: cover; }
25
+ .play-icon { font-size: 40px; color: rgba(0,0,0,0.6); opacity: 0.8; transition: opacity 0.2s; position: absolute; }
26
+ .video-item:hover .play-icon { opacity: 1; }
27
+ .video-item-title { font-weight: 500; margin-top: auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
28
+ .pagination { display: flex; justify-content: center; gap: 5px; margin-top: 20px; }
29
+ .page-btn { padding: 8px 12px; background-color: white; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; }
30
+ .page-btn:hover { background-color: #f0f0f0; }
31
+ .page-btn.active { background-color: #333; color: white; border-color: #333; }
32
+ .page-btn.disabled { cursor: default; color: #ccc; }
33
+ .popup-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.85); display: flex; align-items: center; justify-content: center; z-index: 1000; opacity: 0; visibility: hidden; transition: opacity 0.3s, visibility 0.3s; }
34
+ .popup-overlay.active { opacity: 1; visibility: visible; }
35
+ .popup-content { width: 90%; max-width: 1100px; background-color: #000; border-radius: 8px; overflow: hidden; position: relative; box-shadow: 0 10px 25px rgba(0,0,0,0.5); display: flex; flex-direction: column; }
36
+ .close-popup { position: absolute; top: 8px; right: 8px; background-color: rgba(30,30,30,0.7); color: white; border: none; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 1001; font-size: 18px; line-height: 1; }
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...">
97
  </div>
 
98
  <div class="video-list">
99
  <div class="video-grid" id="videoGrid">
100
+ <div class="video-item"><div class="video-thumbnail">Loading...</div></div>
 
 
 
101
  </div>
102
  <div class="pagination" id="pagination"></div>
103
  </div>
104
  </div>
105
 
 
106
  <div class="popup-overlay" id="playerPopup">
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>
 
114
  </div>
115
  </div>
116
 
 
117
  <script src="https://cdn.plyr.io/3.7.8/plyr.js"></script>
 
118
  <script>
119
+ // --- Keep Global variables, DOM Elements as before ---
120
  let videoData = [];
121
  let filteredVideos = [];
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');
131
  const searchInput = document.getElementById('search');
132
  const playerPopup = document.getElementById('playerPopup');
133
+ const videoPlayerElement = document.getElementById('videoPlayer');
134
  const currentTitle = document.getElementById('currentTitle');
135
  const closePopup = document.getElementById('closePopup');
136
 
137
+ // --- fetchVideoData, renderVideoGrid, renderPagination (Keep as before) ---
138
+ async function fetchVideoData() {
 
139
  try {
 
140
  videoGrid.innerHTML = '<div class="no-videos">Loading video list...</div>';
141
  pagination.innerHTML = '';
142
+ const response = await fetch('database.json');
143
+ if (!response.ok) throw new Error(`Failed to fetch video data (Status: ${response.status})`);
 
 
 
144
  const data = await response.json();
145
+ videoData = Array.isArray(data) ? data : (data.videos && Array.isArray(data.videos) ? data.videos : []);
 
 
 
 
146
  videoData.forEach(video => {
147
  if (video.url && video.url.includes('huggingface.co')) {
148
+ try {
149
+ let url = new URL(video.url);
150
+ if (url.searchParams.get('download') !== 'true') {
151
+ url.searchParams.set('download', 'true');
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;
167
  }
168
+ filteredVideos = [...videoData];
169
+ currentPage = 1;
 
170
  renderPagination();
171
  renderVideoGrid();
172
  } catch (error) {
173
+ console.error('Error fetching/processing video data:', error);
174
+ videoGrid.innerHTML = `<div class="no-videos">Error loading videos: ${error.message}. Check console and network tab.</div>`;
175
  }
176
  }
177
 
178
  function renderVideoGrid() {
179
+ videoGrid.innerHTML = '';
180
+ if (filteredVideos.length === 0) {
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;
 
188
  const endIndex = Math.min(startIndex + videosPerPage, filteredVideos.length);
189
  const currentVideos = filteredVideos.slice(startIndex, endIndex);
 
190
  const fragment = document.createDocumentFragment();
191
  currentVideos.forEach((video, index) => {
192
  const videoItem = document.createElement('div');
 
201
  `;
202
  videoItem.addEventListener('click', function() {
203
  const dataIndex = parseInt(this.getAttribute('data-index'));
204
+ if (!isNaN(dataIndex) && dataIndex >= 0 && dataIndex < filteredVideos.length) {
205
  playVideo(filteredVideos[dataIndex]);
206
  } else {
207
  console.error("Invalid video index clicked:", dataIndex);
 
209
  });
210
  fragment.appendChild(videoItem);
211
  });
212
+ videoGrid.appendChild(fragment);
 
213
  }
214
 
215
  function renderPagination() {
216
  const totalPages = Math.ceil(filteredVideos.length / videosPerPage);
217
+ pagination.innerHTML = '';
218
+ if (totalPages <= 1) return;
219
+ let buttons = [];
220
+ const maxVisibleButtons = 7;
221
+ if (totalPages <= maxVisibleButtons) {
222
+ for (let i = 1; i <= totalPages; i++) buttons.push(i);
 
 
 
 
 
 
 
223
  } else {
224
+ buttons.push(1);
225
  let startPage, endPage;
 
226
  if (currentPage <= Math.floor(maxVisibleButtons / 2) + 1) {
227
+ startPage = 2; endPage = maxVisibleButtons - 2;
 
 
228
  for (let i = startPage; i <= endPage; i++) buttons.push(i);
229
  buttons.push('...');
230
  } else if (currentPage >= totalPages - Math.floor(maxVisibleButtons / 2)) {
231
+ startPage = totalPages - (maxVisibleButtons - 3);
 
232
  buttons.push('...');
233
  for (let i = startPage; i < totalPages; i++) buttons.push(i);
234
  } else {
 
235
  buttons.push('...');
236
+ const sideButtons = Math.floor((maxVisibleButtons - 3) / 2);
237
+ startPage = currentPage - sideButtons; endPage = currentPage + sideButtons;
 
238
  for (let i = startPage; i <= endPage; i++) buttons.push(i);
239
  buttons.push('...');
240
  }
241
+ buttons.push(totalPages);
242
  }
243
+ pagination.innerHTML = buttons.map(button => {
244
+ if (button === '...') return '<span class="page-btn disabled">...</span>';
 
 
 
 
245
  return `<button class="page-btn ${button === currentPage ? 'active' : ''}" data-page="${button}">${button}</button>`;
246
  }).join('');
247
+ document.querySelectorAll('.page-btn[data-page]').forEach(button => {
 
 
248
  button.addEventListener('click', function() {
249
  const newPage = parseInt(this.getAttribute('data-page'));
250
  if (newPage !== currentPage) {
251
+ currentPage = newPage; renderPagination(); renderVideoGrid();
252
+ window.scrollTo({ top: videoGrid.offsetTop - 20, behavior: 'smooth' });
 
 
253
  }
254
  });
255
  });
256
  }
257
 
258
 
259
+ // --- Updated Player Logic ---
260
 
261
  function playVideo(video) {
262
  if (!video || !video.url) {
 
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);
396
+ }
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
  });
 
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();
454