Subham9126 commited on
Commit
8a8047d
·
verified ·
1 Parent(s): c344676

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +660 -383
index.html CHANGED
@@ -3,427 +3,704 @@
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; }
17
- .header { background-color: #333; color: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
18
- .search-container { padding: 15px; background-color: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
19
- #search { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 16px; }
20
- .video-list { background-color: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); padding: 15px; }
21
- .video-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 15px; }
22
- .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; }
23
- .video-item:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); }
24
- .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; }
25
- .video-thumbnail img { width: 100%; height: 100%; object-fit: cover; }
26
- .play-icon { font-size: 40px; color: rgba(0,0,0,0.6); opacity: 0.8; transition: opacity 0.2s; position: absolute; }
27
- .video-item:hover .play-icon { opacity: 1; }
28
- .video-item-title { font-weight: 500; margin-top: auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
29
- .pagination { display: flex; justify-content: center; gap: 5px; margin-top: 20px; }
30
- .page-btn { padding: 8px 12px; background-color: white; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; }
31
- .page-btn:hover { background-color: #f0f0f0; }
32
- .page-btn.active { background-color: #333; color: white; border-color: #333; }
33
- .page-btn.disabled { cursor: default; color: #ccc; }
34
- .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; }
35
- .popup-overlay.active { opacity: 1; visibility: visible; }
36
- .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; }
37
- .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; }
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...">
58
- </div>
59
- <div class="video-list">
60
- <div class="video-grid" id="videoGrid">
61
- <div class="video-item"><div class="video-thumbnail">Loading...</div></div>
62
  </div>
63
- <div class="pagination" id="pagination"></div>
64
- </div>
 
 
 
 
 
 
65
  </div>
66
-
67
- <div class="popup-overlay" id="playerPopup">
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>
74
  </div>
75
  </div>
76
-
77
- <script src="https://cdn.plyr.io/3.7.8/plyr.js"></script>
 
 
78
  <script>
79
- // --- Keep Global variables, DOM Elements as before ---
80
- let videoData = [];
81
- let filteredVideos = [];
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');
91
- const searchInput = document.getElementById('search');
92
- const playerPopup = document.getElementById('playerPopup');
93
- const videoPlayerElement = document.getElementById('videoPlayer');
94
- const currentTitle = document.getElementById('currentTitle');
95
- const closePopup = document.getElementById('closePopup');
96
-
97
- // --- fetchVideoData, renderVideoGrid, renderPagination (Keep as before) ---
98
- async function fetchVideoData() {
 
 
 
 
 
 
 
 
 
 
 
99
  try {
100
- videoGrid.innerHTML = '<div class="no-videos">Loading video list...</div>';
101
- pagination.innerHTML = '';
102
- const response = await fetch('database.json');
103
- if (!response.ok) throw new Error(`Failed to fetch video data (Status: ${response.status})`);
 
 
 
 
 
104
  const data = await response.json();
105
- videoData = Array.isArray(data) ? data : (data.videos && Array.isArray(data.videos) ? data.videos : []);
106
- videoData.forEach(video => {
107
- if (video.url && video.url.includes('huggingface.co')) {
108
- try {
109
- let url = new URL(video.url);
110
- if (url.searchParams.get('download') !== 'true') {
111
- url.searchParams.set('download', 'true');
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;
128
  }
129
- filteredVideos = [...videoData];
130
- currentPage = 1;
131
- renderPagination();
 
 
 
 
 
 
 
132
  renderVideoGrid();
 
 
133
  } catch (error) {
134
- console.error('Error fetching/processing video data:', error);
135
- videoGrid.innerHTML = `<div class="no-videos">Error loading videos: ${error.message}. Check console and network tab.</div>`;
 
 
 
 
 
 
 
136
  }
137
  }
138
-
 
139
  function renderVideoGrid() {
140
- videoGrid.innerHTML = '';
141
- if (filteredVideos.length === 0) {
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;
149
- const endIndex = Math.min(startIndex + videosPerPage, filteredVideos.length);
150
- const currentVideos = filteredVideos.slice(startIndex, endIndex);
151
- const fragment = document.createDocumentFragment();
152
- currentVideos.forEach((video, index) => {
153
- const videoItem = document.createElement('div');
154
- videoItem.className = 'video-item';
155
- videoItem.setAttribute('data-index', startIndex + index);
156
- videoItem.innerHTML = `
157
  <div class="video-thumbnail">
158
- <div class="play-icon">▶</div>
159
- ${video.thumbnail ? `<img src="${video.thumbnail}" alt="${video.title} thumbnail" loading="lazy">` : ''}
 
 
 
 
160
  </div>
161
- <div class="video-item-title" title="${video.title}">${video.title}</div>
162
  `;
163
- videoItem.addEventListener('click', function() {
164
- const dataIndex = parseInt(this.getAttribute('data-index'));
165
- if (!isNaN(dataIndex) && dataIndex >= 0 && dataIndex < filteredVideos.length) {
166
- playVideo(filteredVideos[dataIndex]);
167
- } else {
168
- console.error("Invalid video index clicked:", dataIndex);
169
- }
170
- });
171
- fragment.appendChild(videoItem);
172
  });
173
- videoGrid.appendChild(fragment);
174
  }
175
-
 
176
  function renderPagination() {
177
- const totalPages = Math.ceil(filteredVideos.length / videosPerPage);
178
- pagination.innerHTML = '';
179
- if (totalPages <= 1) return;
180
- let buttons = [];
181
- const maxVisibleButtons = 7;
182
- if (totalPages <= maxVisibleButtons) {
183
- for (let i = 1; i <= totalPages; i++) buttons.push(i);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  } else {
185
- buttons.push(1);
186
- let startPage, endPage;
187
- if (currentPage <= Math.floor(maxVisibleButtons / 2) + 1) {
188
- startPage = 2; endPage = maxVisibleButtons - 2;
189
- for (let i = startPage; i <= endPage; i++) buttons.push(i);
190
- buttons.push('...');
191
- } else if (currentPage >= totalPages - Math.floor(maxVisibleButtons / 2)) {
192
- startPage = totalPages - (maxVisibleButtons - 3);
193
- buttons.push('...');
194
- for (let i = startPage; i < totalPages; i++) buttons.push(i);
195
- } else {
196
- buttons.push('...');
197
- const sideButtons = Math.floor((maxVisibleButtons - 3) / 2);
198
- startPage = currentPage - sideButtons; endPage = currentPage + sideButtons;
199
- for (let i = startPage; i <= endPage; i++) buttons.push(i);
200
- buttons.push('...');
201
  }
202
- buttons.push(totalPages);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  }
204
- pagination.innerHTML = buttons.map(button => {
205
- if (button === '...') return '<span class="page-btn disabled">...</span>';
206
- return `<button class="page-btn ${button === currentPage ? 'active' : ''}" data-page="${button}">${button}</button>`;
207
- }).join('');
208
- document.querySelectorAll('.page-btn[data-page]').forEach(button => {
209
- button.addEventListener('click', function() {
210
- const newPage = parseInt(this.getAttribute('data-page'));
211
- if (newPage !== currentPage) {
212
- currentPage = newPage; renderPagination(); renderVideoGrid();
213
- window.scrollTo({ top: videoGrid.offsetTop - 20, behavior: 'smooth' });
214
- }
215
  });
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);
402
- }
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();
426
-
427
  </script>
428
  </body>
429
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>HF Video Library</title>
7
+ <!-- Plyr.js CSS -->
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/plyr/3.7.8/plyr.min.css">
 
 
9
  <style>
10
+ /* Global Styles */
11
+ * {
12
+ box-sizing: border-box;
13
+ margin: 0;
14
+ padding: 0;
15
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
+ }
17
+
18
+ body {
19
+ background-color: #f5f5f5;
20
+ color: #333;
21
+ line-height: 1.6;
22
+ padding: 20px;
23
+ }
24
+
25
+ .container {
26
+ max-width: 1200px;
27
+ margin: 0 auto;
28
+ padding: 20px;
29
+ }
30
+
31
+ /* Header Styles */
32
+ header {
33
+ text-align: center;
34
+ margin-bottom: 30px;
35
+ }
36
+
37
+ h1 {
38
+ font-size: 2.5rem;
39
+ color: #333;
40
+ margin-bottom: 10px;
41
+ }
42
+
43
+ /* Search Bar */
44
+ .search-container {
45
+ margin-bottom: 30px;
46
+ display: flex;
47
+ justify-content: center;
48
+ }
49
+
50
+ #search-input {
51
+ width: 100%;
52
+ max-width: 500px;
53
+ padding: 12px 20px;
54
+ border: 1px solid #ddd;
55
+ border-radius: 25px;
56
+ font-size: 16px;
57
+ transition: all 0.3s ease;
58
+ }
59
+
60
+ #search-input:focus {
61
+ outline: none;
62
+ border-color: #2196F3;
63
+ box-shadow: 0 0 8px rgba(33, 150, 243, 0.4);
64
+ }
65
+
66
+ /* Video Grid */
67
+ .video-grid {
68
+ display: grid;
69
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
70
+ gap: 20px;
71
+ margin-bottom: 30px;
72
+ }
73
+
74
+ .video-card {
75
+ background: white;
76
+ border-radius: 8px;
77
+ overflow: hidden;
78
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
79
+ cursor: pointer;
80
+ transition: transform 0.2s ease;
81
+ }
82
+
83
+ .video-card:hover {
84
+ transform: translateY(-5px);
85
+ box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15);
86
+ }
87
+
88
+ .video-thumbnail {
89
+ width: 100%;
90
+ height: 150px;
91
+ background-color: #333;
92
+ display: flex;
93
+ justify-content: center;
94
+ align-items: center;
95
+ color: white;
96
+ }
97
+
98
+ .video-info {
99
+ padding: 15px;
100
+ }
101
+
102
+ .video-title {
103
+ font-size: 16px;
104
+ font-weight: 600;
105
+ overflow: hidden;
106
+ text-overflow: ellipsis;
107
+ display: -webkit-box;
108
+ -webkit-line-clamp: 2;
109
+ -webkit-box-orient: vertical;
110
+ }
111
+
112
+ /* Pagination */
113
+ .pagination {
114
+ display: flex;
115
+ justify-content: center;
116
+ margin-top: 20px;
117
+ gap: 5px;
118
+ }
119
+
120
+ .page-btn {
121
+ padding: 8px 15px;
122
+ border: 1px solid #ddd;
123
+ background-color: white;
124
+ border-radius: 4px;
125
+ cursor: pointer;
126
+ transition: all 0.2s ease;
127
+ }
128
+
129
+ .page-btn:hover {
130
+ background-color: #f0f0f0;
131
+ }
132
+
133
+ .page-btn.active {
134
+ background-color: #2196F3;
135
+ color: white;
136
+ border-color: #2196F3;
137
+ }
138
+
139
+ /* Video Modal */
140
+ .modal-overlay {
141
+ position: fixed;
142
+ top: 0;
143
+ left: 0;
144
+ width: 100%;
145
+ height: 100%;
146
+ background-color: rgba(0, 0, 0, 0.8);
147
+ display: flex;
148
+ justify-content: center;
149
+ align-items: center;
150
+ z-index: 1000;
151
+ opacity: 0;
152
+ visibility: hidden;
153
+ transition: opacity 0.3s ease, visibility 0.3s ease;
154
+ }
155
+
156
+ .modal-overlay.active {
157
+ opacity: 1;
158
+ visibility: visible;
159
+ }
160
+
161
+ .modal-content {
162
+ width: 90%;
163
+ max-width: 900px;
164
+ background-color: #000;
165
+ border-radius: 8px;
166
+ overflow: hidden;
167
+ position: relative;
168
+ }
169
+
170
+ .video-player-container {
171
+ width: 100%;
172
+ position: relative;
173
+ }
174
+
175
+ .close-modal {
176
+ position: absolute;
177
+ top: 15px;
178
+ right: 15px;
179
+ color: white;
180
+ background-color: rgba(0, 0, 0, 0.5);
181
+ width: 30px;
182
+ height: 30px;
183
+ border-radius: 50%;
184
+ display: flex;
185
+ justify-content: center;
186
+ align-items: center;
187
+ cursor: pointer;
188
+ z-index: 10;
189
+ font-size: 18px;
190
+ }
191
+
192
+ /* Loading State */
193
+ .loading {
194
+ text-align: center;
195
+ padding: 50px;
196
+ font-size: 18px;
197
+ color: #666;
198
+ }
199
+
200
+ /* Error Message */
201
+ .error-message {
202
+ text-align: center;
203
+ padding: 30px;
204
+ color: #d32f2f;
205
+ background-color: #ffebee;
206
+ border-radius: 4px;
207
+ margin: 20px 0;
208
+ border: 1px solid #ffcdd2;
209
+ }
210
+
211
+ /* No Results */
212
+ .no-results {
213
+ text-align: center;
214
+ padding: 40px;
215
+ font-size: 18px;
216
+ color: #666;
217
+ }
218
+
219
+ /* Responsive Adjustments */
220
+ @media (max-width: 768px) {
221
+ .video-grid {
222
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
223
+ gap: 15px;
224
+ }
225
+
226
+ .video-thumbnail {
227
+ height: 100px;
228
+ }
229
+
230
+ .video-info {
231
+ padding: 10px;
232
+ }
233
+
234
+ .video-title {
235
+ font-size: 14px;
236
+ }
237
+
238
+ .modal-content {
239
+ width: 95%;
240
+ }
241
+ }
242
  </style>
243
  </head>
244
  <body>
245
+ <div class="container">
246
+ <header>
247
+ <h1>HF Video Library</h1>
248
+ <div class="search-container">
249
+ <input type="text" id="search-input" placeholder="Search videos...">
 
 
 
 
 
 
250
  </div>
251
+ </header>
252
+
253
+ <div id="loading" class="loading">Loading videos...</div>
254
+ <div id="error-container"></div>
255
+
256
+ <div id="video-grid" class="video-grid"></div>
257
+
258
+ <div id="pagination" class="pagination"></div>
259
  </div>
260
+
261
+ <div id="video-modal" class="modal-overlay">
262
+ <div class="modal-content">
263
+ <div class="close-modal" id="close-modal">✕</div>
264
+ <div class="video-player-container">
265
+ <video id="video-player" controls crossorigin playsinline></video>
266
+ </div>
 
267
  </div>
268
  </div>
269
+
270
+ <!-- Plyr.js -->
271
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/plyr/3.7.8/plyr.min.js"></script>
272
+
273
  <script>
274
+ // Configuration
275
+ const CONFIG = {
276
+ videosPerPage: 12,
277
+ databasePath: './database.json'
278
+ };
279
+
280
+ // State management
281
+ let state = {
282
+ videos: [],
283
+ filteredVideos: [],
284
+ currentPage: 1,
285
+ searchQuery: '',
286
+ player: null,
287
+ currentVideoUrl: null,
288
+ timeoutIds: []
289
+ };
290
+
291
+ // DOM elements
292
+ const elements = {
293
+ videoGrid: document.getElementById('video-grid'),
294
+ pagination: document.getElementById('pagination'),
295
+ searchInput: document.getElementById('search-input'),
296
+ videoModal: document.getElementById('video-modal'),
297
+ videoPlayer: document.getElementById('video-player'),
298
+ closeModal: document.getElementById('close-modal'),
299
+ loading: document.getElementById('loading'),
300
+ errorContainer: document.getElementById('error-container')
301
+ };
302
+
303
+ // Fetch video data from database.json
304
+ async function fetchVideoData() {
305
  try {
306
+ elements.loading.style.display = 'block';
307
+ elements.errorContainer.innerHTML = '';
308
+
309
+ const response = await fetch(CONFIG.databasePath);
310
+
311
+ if (!response.ok) {
312
+ throw new Error(`Failed to load video database (${response.status})`);
313
+ }
314
+
315
  const data = await response.json();
316
+
317
+ if (!data || !Array.isArray(data.videos)) {
318
+ throw new Error('Invalid database format');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  }
320
+
321
+ // Filter out invalid entries (missing required fields)
322
+ state.videos = data.videos.filter(video =>
323
+ video && typeof video.title === 'string' &&
324
+ typeof video.url === 'string' &&
325
+ video.title.trim() !== '' &&
326
+ video.url.trim() !== ''
327
+ );
328
+
329
+ state.filteredVideos = [...state.videos];
330
  renderVideoGrid();
331
+ renderPagination();
332
+
333
  } catch (error) {
334
+ console.error('Error loading video data:', error);
335
+ elements.errorContainer.innerHTML = `
336
+ <div class="error-message">
337
+ Failed to load videos. Please try again later.
338
+ <br><small>${error.message}</small>
339
+ </div>
340
+ `;
341
+ } finally {
342
+ elements.loading.style.display = 'none';
343
  }
344
  }
345
+
346
+ // Render the video grid
347
  function renderVideoGrid() {
348
+ const startIndex = (state.currentPage - 1) * CONFIG.videosPerPage;
349
+ const endIndex = startIndex + CONFIG.videosPerPage;
350
+ const videosToDisplay = state.filteredVideos.slice(startIndex, endIndex);
351
+
352
+ elements.videoGrid.innerHTML = '';
353
+
354
+ if (videosToDisplay.length === 0) {
355
+ elements.videoGrid.innerHTML = `
356
+ <div class="no-results">
357
+ ${state.searchQuery ? 'No videos match your search.' : 'No videos available.'}
358
+ </div>
359
+ `;
360
  return;
361
  }
362
+
363
+ videosToDisplay.forEach((video, index) => {
364
+ const videoCard = document.createElement('div');
365
+ videoCard.className = 'video-card';
366
+ videoCard.dataset.url = video.url;
367
+ videoCard.dataset.title = video.title;
368
+ videoCard.dataset.index = startIndex + index;
369
+
370
+ videoCard.innerHTML = `
371
  <div class="video-thumbnail">
372
+ <svg width="50" height="50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
373
+ <polygon points="5 3 19 12 5 21 5 3"></polygon>
374
+ </svg>
375
+ </div>
376
+ <div class="video-info">
377
+ <h3 class="video-title">${video.title}</h3>
378
  </div>
 
379
  `;
380
+
381
+ videoCard.addEventListener('click', () => playVideo(video));
382
+ elements.videoGrid.appendChild(videoCard);
 
 
 
 
 
 
383
  });
 
384
  }
385
+
386
+ // Render pagination controls
387
  function renderPagination() {
388
+ const totalPages = Math.ceil(state.filteredVideos.length / CONFIG.videosPerPage);
389
+
390
+ elements.pagination.innerHTML = '';
391
+
392
+ if (totalPages <= 1) {
393
+ return;
394
+ }
395
+
396
+ // Previous button
397
+ if (state.currentPage > 1) {
398
+ const prevBtn = createPaginationButton('←', () => {
399
+ state.currentPage--;
400
+ renderVideoGrid();
401
+ renderPagination();
402
+ window.scrollTo(0, 0);
403
+ });
404
+ elements.pagination.appendChild(prevBtn);
405
+ }
406
+
407
+ // Page numbers with ellipsis
408
+ const renderPageBtn = (page) => {
409
+ const btn = createPaginationButton(page, () => {
410
+ state.currentPage = page;
411
+ renderVideoGrid();
412
+ renderPagination();
413
+ window.scrollTo(0, 0);
414
+ }, page === state.currentPage);
415
+ elements.pagination.appendChild(btn);
416
+ };
417
+
418
+ // Logic for showing page numbers with ellipsis
419
+ if (totalPages <= 7) {
420
+ // Show all pages if there are 7 or fewer
421
+ for (let i = 1; i <= totalPages; i++) {
422
+ renderPageBtn(i);
423
+ }
424
  } else {
425
+ // Show first page
426
+ renderPageBtn(1);
427
+
428
+ // Show ellipsis or pages
429
+ if (state.currentPage > 3) {
430
+ elements.pagination.appendChild(createEllipsis());
 
 
 
 
 
 
 
 
 
 
431
  }
432
+
433
+ // Show current page and surrounding pages
434
+ let startPage = Math.max(2, state.currentPage - 1);
435
+ let endPage = Math.min(totalPages - 1, state.currentPage + 1);
436
+
437
+ // Adjust to show at least 3 pages (if possible)
438
+ if (startPage === 2) endPage = Math.min(totalPages - 1, 4);
439
+ if (endPage === totalPages - 1) startPage = Math.max(2, totalPages - 3);
440
+
441
+ for (let i = startPage; i <= endPage; i++) {
442
+ renderPageBtn(i);
443
+ }
444
+
445
+ // Show ellipsis or pages
446
+ if (state.currentPage < totalPages - 2) {
447
+ elements.pagination.appendChild(createEllipsis());
448
+ }
449
+
450
+ // Show last page
451
+ renderPageBtn(totalPages);
452
  }
453
+
454
+ // Next button
455
+ if (state.currentPage < totalPages) {
456
+ const nextBtn = createPaginationButton('', () => {
457
+ state.currentPage++;
458
+ renderVideoGrid();
459
+ renderPagination();
460
+ window.scrollTo(0, 0);
 
 
 
461
  });
462
+ elements.pagination.appendChild(nextBtn);
463
+ }
464
  }
465
+
466
+ // Create pagination button
467
+ function createPaginationButton(text, onClick, isActive = false) {
468
+ const button = document.createElement('button');
469
+ button.className = `page-btn ${isActive ? 'active' : ''}`;
470
+ button.textContent = text;
471
+ button.addEventListener('click', onClick);
472
+ return button;
473
+ }
474
+
475
+ // Create ellipsis element for pagination
476
+ function createEllipsis() {
477
+ const ellipsis = document.createElement('span');
478
+ ellipsis.className = 'page-btn';
479
+ ellipsis.textContent = '...';
480
+ ellipsis.style.cursor = 'default';
481
+ return ellipsis;
482
+ }
483
+
484
+ // Search functionality
485
+ function setupSearch() {
486
+ elements.searchInput.addEventListener('input', (e) => {
487
+ const query = e.target.value.trim().toLowerCase();
488
+ state.searchQuery = query;
489
+
490
+ if (query === '') {
491
+ state.filteredVideos = [...state.videos];
492
+ } else {
493
+ state.filteredVideos = state.videos.filter(video =>
494
+ video.title.toLowerCase().includes(query)
495
+ );
496
  }
497
+
498
+ state.currentPage = 1;
499
+ renderVideoGrid();
500
+ renderPagination();
501
+ });
502
+ }
503
+
504
+ // Process URL for Hugging Face
505
+ function processVideoUrl(url) {
506
+ // Add download=true parameter to HF URLs if not already present
507
+ if (url.includes('huggingface.co') && !url.includes('download=true')) {
508
+ const separator = url.includes('?') ? '&' : '?';
509
+ return `${url}${separator}download=true`;
510
  }
511
+ return url;
512
+ }
513
+
514
+ // Play video function
 
 
 
 
 
 
 
 
 
 
515
  function playVideo(video) {
516
+ // Clean up any existing player properly before initializing a new one
517
+ cleanupPlayer();
518
+
519
+ // Set current video URL
520
+ state.currentVideoUrl = processVideoUrl(video.url);
521
+
522
+ // Setup video element
523
+ elements.videoPlayer.setAttribute('src', state.currentVideoUrl);
524
+ elements.videoPlayer.setAttribute('data-title', video.title);
525
+
526
+ // Show modal
527
+ elements.videoModal.classList.add('active');
528
+ document.body.style.overflow = 'hidden'; // Prevent scrolling
529
+
530
+ // Initialize Plyr after a small delay to ensure the DOM is updated
531
+ setTimeout(() => {
532
+ // Initialize player
533
+ state.player = new Plyr('#video-player', {
534
+ controls: [
535
+ 'play-large', 'play', 'progress', 'current-time', 'mute',
536
+ 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'
537
+ ],
538
+ resetOnEnd: true,
539
+ keyboard: { focused: true, global: false },
540
+ title: video.title
 
 
 
 
541
  });
542
+
543
+ // Start playing
544
+ const playPromise = elements.videoPlayer.play();
545
+ if (playPromise !== undefined) {
546
+ playPromise.catch(error => {
547
+ console.log('Auto-play was prevented:', error);
548
+ // Auto-play was prevented, show play button
549
+ });
550
+ }
551
+
552
+ // Setup double-tap handling for mobile
553
+ setupDoubleTapControls();
554
+ }, 100);
555
+ }
556
+
557
+ // Cleanup player function
558
+ function cleanupPlayer() {
559
+ // Clear any remaining timeouts
560
+ state.timeoutIds.forEach(id => clearTimeout(id));
561
+ state.timeoutIds = [];
562
+
563
+ // Destroy Plyr instance if it exists
564
+ if (state.player) {
565
+ // Stop video playback before destroying
566
+ if (elements.videoPlayer && !elements.videoPlayer.paused) {
567
+ elements.videoPlayer.pause();
568
+ }
569
+
570
+ // Reset source and current time
571
+ elements.videoPlayer.removeAttribute('src');
572
+ elements.videoPlayer.load();
573
+
574
+ // Destroy player
575
+ state.player.destroy();
576
+ state.player = null;
577
+ }
578
+
579
+ // Additional cleanup to ensure no audio continues
580
+ if (elements.videoPlayer) {
581
+ elements.videoPlayer.currentTime = 0;
582
+ elements.videoPlayer.src = "";
583
+ elements.videoPlayer.load();
584
+ }
585
+
586
+ // Clear URL reference
587
+ state.currentVideoUrl = null;
588
+ }
589
+
590
+ // Setup double-tap controls for mobile
591
+ function setupDoubleTapControls() {
592
+ if (!state.player || !elements.videoPlayer) return;
593
+
594
+ const videoContainer = elements.videoPlayer.closest('.video-player-container');
595
+ if (!videoContainer) return;
596
+
597
+ let lastTap = 0;
598
+ let tapTimeout;
599
+
600
+ // Create left and right tap areas (invisible overlays)
601
+ const createTapArea = (side) => {
602
+ const area = document.createElement('div');
603
+ area.style.position = 'absolute';
604
+ area.style.top = '0';
605
+ area.style.height = '100%';
606
+ area.style.width = '50%';
607
+ area.style[side] = '0';
608
+ area.style.zIndex = '5';
609
+
610
+ area.addEventListener('touchstart', (e) => {
611
+ const currentTime = new Date().getTime();
612
+ const tapLength = currentTime - lastTap;
613
+
614
+ clearTimeout(tapTimeout);
615
+
616
+ if (tapLength < 500 && tapLength > 0) {
617
+ // Double tap detected
618
+ e.preventDefault();
619
+ if (side === 'left') {
620
+ // Double tap left - seek backward 10 seconds
621
+ state.player.currentTime -= 10;
622
+ } else {
623
+ // Double tap right - seek forward 10 seconds
624
+ state.player.currentTime += 10;
625
+ }
626
  } else {
627
+ // Single tap - wait to see if it becomes a double tap
628
+ tapTimeout = setTimeout(() => {
629
+ // Single tap action - toggle play/pause
630
+ if (state.player.paused) {
631
+ state.player.play();
632
+ } else {
633
+ state.player.pause();
634
+ }
635
+ }, 500);
636
+ }
637
+
638
+ lastTap = currentTime;
639
+ }, false);
640
+
641
+ return area;
642
+ };
643
+
644
+ // Add areas to container
645
+ const leftArea = createTapArea('left');
646
+ const rightArea = createTapArea('right');
647
+
648
+ videoContainer.appendChild(leftArea);
649
+ videoContainer.appendChild(rightArea);
650
+
651
+ // Store references to remove them later
652
+ state.tapAreas = [leftArea, rightArea];
653
+ }
654
+
655
+ // Close modal and stop video
656
+ function closeModal() {
657
+ // First, clean up the player
658
+ cleanupPlayer();
659
+
660
+ // Remove tap areas if they exist
661
+ if (state.tapAreas) {
662
+ state.tapAreas.forEach(area => {
663
+ if (area && area.parentNode) {
664
+ area.parentNode.removeChild(area);
665
+ }
666
  });
667
+ state.tapAreas = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
668
  }
669
+
670
+ // Hide modal
671
+ elements.videoModal.classList.remove('active');
672
+ document.body.style.overflow = ''; // Restore scrolling
673
+ }
674
+
675
+ // Setup modal close handlers
676
+ function setupModalHandlers() {
677
+ // Close button
678
+ elements.closeModal.addEventListener('click', closeModal);
679
+
680
+ // Click outside modal content
681
+ elements.videoModal.addEventListener('click', (e) => {
682
+ if (e.target === elements.videoModal) {
683
+ closeModal();
684
+ }
685
+ });
686
+
687
+ // Escape key
688
+ document.addEventListener('keydown', (e) => {
689
+ if (e.key === 'Escape' && elements.videoModal.classList.contains('active')) {
690
+ closeModal();
691
+ }
692
+ });
693
+ }
694
+
695
+ // Initialize application
696
+ function init() {
697
+ fetchVideoData();
698
+ setupSearch();
699
+ setupModalHandlers();
700
  }
701
+
702
+ // Start the app when DOM is loaded
703
+ document.addEventListener('DOMContentLoaded', init);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
704
  </script>
705
  </body>
706
  </html>