Subham9126 commited on
Commit
3f097bd
·
verified ·
1 Parent(s): bb5b26d

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +382 -606
index.html CHANGED
@@ -4,703 +4,479 @@
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>
 
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>HF Video Library</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css" />
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
10
  <style>
11
+ /* Custom CSS for animations and overrides */
12
+ .fade-in {
13
+ animation: fadeIn 0.3s ease-in-out;
 
 
 
14
  }
15
 
16
+ @keyframes fadeIn {
17
+ from { opacity: 0; }
18
+ to { opacity: 1; }
 
 
19
  }
20
 
21
+ .video-card:hover .video-overlay {
22
+ opacity: 1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  }
24
 
25
+ .video-overlay {
26
+ transition: opacity 0.2s ease;
 
 
 
27
  }
28
 
29
+ .player-modal {
 
 
 
 
 
 
30
  transition: all 0.3s ease;
31
  }
32
 
33
+ .player-modal.hidden {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  opacity: 0;
35
  visibility: hidden;
36
+ transform: translateY(20px);
37
  }
38
 
39
+ .player-modal.visible {
40
  opacity: 1;
41
  visibility: visible;
42
+ transform: translateY(0);
43
  }
44
 
45
+ /* Custom scrollbar */
46
+ ::-webkit-scrollbar {
47
+ width: 8px;
48
+ height: 8px;
 
 
 
 
 
 
 
 
49
  }
50
 
51
+ ::-webkit-scrollbar-track {
52
+ background: #f1f1f1;
53
+ border-radius: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
54
  }
55
 
56
+ ::-webkit-scrollbar-thumb {
57
+ background: #888;
58
+ border-radius: 10px;
 
 
 
59
  }
60
 
61
+ ::-webkit-scrollbar-thumb:hover {
62
+ background: #555;
 
 
 
 
 
 
 
63
  }
64
 
65
+ /* Loading spinner */
66
+ .spinner {
67
+ width: 40px;
68
+ height: 40px;
69
+ border: 4px solid rgba(0, 0, 0, 0.1);
70
+ border-left-color: #3b82f6;
71
+ border-radius: 50%;
72
+ animation: spin 1s linear infinite;
73
  }
74
 
75
+ @keyframes spin {
76
+ to { transform: rotate(360deg); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  }
78
  </style>
79
  </head>
80
+ <body class="bg-gray-100 font-sans antialiased">
81
+ <!-- Header -->
82
+ <header class="bg-gradient-to-r from-blue-600 to-indigo-700 shadow-lg">
83
+ <div class="container mx-auto px-4 py-6">
84
+ <div class="flex flex-col md:flex-row justify-between items-center">
85
+ <div class="flex items-center mb-4 md:mb-0">
86
+ <i class="fas fa-video text-white text-2xl mr-3"></i>
87
+ <h1 class="text-2xl md:text-3xl font-bold text-white">HF Video Library</h1>
88
+ </div>
89
+ <div class="relative w-full md:w-64">
90
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
91
+ <i class="fas fa-search text-gray-400"></i>
92
+ </div>
93
+ <input
94
+ type="text"
95
+ id="searchInput"
96
+ placeholder="Search videos..."
97
+ class="w-full pl-10 pr-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 bg-white bg-opacity-90 text-gray-800 placeholder-gray-500 transition duration-200"
98
+ >
99
+ </div>
100
  </div>
101
+ </div>
102
+ </header>
103
+
104
+ <!-- Main Content -->
105
+ <main class="container mx-auto px-4 py-8">
106
+ <!-- Loading State -->
107
+ <div id="loadingState" class="flex flex-col items-center justify-center py-20">
108
+ <div class="spinner mb-4"></div>
109
+ <p class="text-gray-600 text-lg">Loading video library...</p>
110
+ </div>
111
+
112
+ <!-- Error State -->
113
+ <div id="errorState" class="hidden flex-col items-center justify-center py-20 text-center">
114
+ <i class="fas fa-exclamation-triangle text-red-500 text-5xl mb-4"></i>
115
+ <h2 class="text-2xl font-semibold text-gray-800 mb-2">Failed to load videos</h2>
116
+ <p class="text-gray-600 mb-6 max-w-md">We couldn't load the video library. Please check your internet connection and try again.</p>
117
+ <button id="retryButton" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg transition duration-200">
118
+ <i class="fas fa-sync-alt mr-2"></i> Retry
119
+ </button>
120
+ </div>
121
+
122
+ <!-- Video Grid -->
123
+ <div id="videoGrid" class="hidden">
124
+ <div class="flex justify-between items-center mb-6">
125
+ <h2 class="text-xl font-semibold text-gray-800">
126
+ <span id="videoCount">0</span> videos available
127
+ </h2>
128
+ <div class="flex items-center space-x-2">
129
+ <span class="text-sm text-gray-600">Items per page:</span>
130
+ <select id="itemsPerPage" class="border rounded px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500">
131
+ <option value="12">12</option>
132
+ <option value="24">24</option>
133
+ <option value="48">48</option>
134
+ </select>
135
+ </div>
136
+ </div>
137
+
138
+ <div id="videoContainer" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"></div>
139
+
140
+ <!-- Pagination -->
141
+ <div id="pagination" class="mt-10 flex justify-center">
142
+ <nav class="flex items-center space-x-1">
143
+ <button id="prevPage" class="px-3 py-1 rounded border bg-white text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed">
144
+ <i class="fas fa-chevron-left"></i>
145
+ </button>
146
+
147
+ <div id="pageNumbers" class="flex space-x-1"></div>
148
+
149
+ <button id="nextPage" class="px-3 py-1 rounded border bg-white text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed">
150
+ <i class="fas fa-chevron-right"></i>
151
+ </button>
152
+ </nav>
153
+ </div>
154
+ </div>
155
+ </main>
156
+
157
+ <!-- Video Player Modal -->
158
+ <div id="playerModal" class="player-modal hidden fixed inset-0 z-50 overflow-y-auto bg-black bg-opacity-75 flex items-center justify-center p-4">
159
+ <div class="relative w-full max-w-4xl bg-white rounded-xl shadow-2xl overflow-hidden">
160
+ <div class="absolute top-4 right-4 z-10">
161
+ <button id="closePlayer" class="bg-gray-800 bg-opacity-70 text-white rounded-full p-2 hover:bg-opacity-100 transition duration-200">
162
+ <i class="fas fa-times"></i>
163
+ </button>
164
+ </div>
165
+
166
+ <div class="aspect-w-16 aspect-h-9 bg-black">
167
+ <div id="playerContainer" class="w-full">
168
+ <video id="player" playsinline controls></video>
169
+ </div>
170
+ </div>
171
+
172
+ <div class="p-4 bg-gray-50">
173
+ <h2 id="videoTitle" class="text-xl font-semibold text-gray-800 mb-2"></h2>
174
+ <div class="flex justify-between items-center text-sm text-gray-600">
175
+ <div id="videoInfo"></div>
176
+ <div id="videoActions" class="flex space-x-3">
177
+ <button id="fullscreenBtn" class="hover:text-blue-600 transition duration-200">
178
+ <i class="fas fa-expand mr-1"></i> Fullscreen
179
+ </button>
180
+ </div>
181
+ </div>
182
  </div>
183
  </div>
184
  </div>
185
+
186
+ <!-- Footer -->
187
+ <footer class="bg-gray-800 text-white py-6">
188
+ <div class="container mx-auto px-4">
189
+ <div class="flex flex-col md:flex-row justify-between items-center">
190
+ <div class="mb-4 md:mb-0">
191
+ <p>&copy; 2023 HF Video Library. All rights reserved.</p>
192
+ </div>
193
+ <div class="flex space-x-4">
194
+ <a href="#" class="hover:text-blue-300 transition duration-200">
195
+ <i class="fab fa-github"></i>
196
+ </a>
197
+ <a href="#" class="hover:text-blue-300 transition duration-200">
198
+ <i class="fab fa-twitter"></i>
199
+ </a>
200
+ <a href="#" class="hover:text-blue-300 transition duration-200">
201
+ <i class="fas fa-envelope"></i>
202
+ </a>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </footer>
207
+
208
+ <script src="https://cdn.plyr.io/3.7.8/plyr.js"></script>
209
  <script>
210
  // Configuration
211
+ const config = {
212
+ databaseUrl: 'database.json',
213
+ itemsPerPage: 12,
214
+ currentPage: 1,
215
+ totalPages: 1,
 
 
216
  videos: [],
217
  filteredVideos: [],
218
+ player: null
 
 
 
 
219
  };
220
+
221
+ // DOM Elements
222
  const elements = {
223
+ loadingState: document.getElementById('loadingState'),
224
+ errorState: document.getElementById('errorState'),
225
+ videoGrid: document.getElementById('videoGrid'),
226
+ videoContainer: document.getElementById('videoContainer'),
227
+ videoCount: document.getElementById('videoCount'),
228
  pagination: document.getElementById('pagination'),
229
+ prevPage: document.getElementById('prevPage'),
230
+ nextPage: document.getElementById('nextPage'),
231
+ pageNumbers: document.getElementById('pageNumbers'),
232
+ itemsPerPage: document.getElementById('itemsPerPage'),
233
+ searchInput: document.getElementById('searchInput'),
234
+ playerModal: document.getElementById('playerModal'),
235
+ closePlayer: document.getElementById('closePlayer'),
236
+ playerContainer: document.getElementById('playerContainer'),
237
+ videoTitle: document.getElementById('videoTitle'),
238
+ videoInfo: document.getElementById('videoInfo'),
239
+ retryButton: document.getElementById('retryButton'),
240
+ fullscreenBtn: document.getElementById('fullscreenBtn')
241
  };
242
+
243
+ // Initialize the application
244
+ document.addEventListener('DOMContentLoaded', () => {
245
+ fetchVideoData();
246
+
247
+ // Event listeners
248
+ elements.searchInput.addEventListener('input', handleSearch);
249
+ elements.itemsPerPage.addEventListener('change', handleItemsPerPageChange);
250
+ elements.prevPage.addEventListener('click', goToPrevPage);
251
+ elements.nextPage.addEventListener('click', goToNextPage);
252
+ elements.closePlayer.addEventListener('click', closeVideoPlayer);
253
+ elements.retryButton.addEventListener('click', fetchVideoData);
254
+ elements.fullscreenBtn.addEventListener('click', toggleFullscreen);
255
+
256
+ // Keyboard shortcuts for player (when modal is open)
257
+ document.addEventListener('keydown', (e) => {
258
+ if (elements.playerModal.classList.contains('visible') && config.player) {
259
+ // Space for play/pause
260
+ if (e.code === 'Space') {
261
+ e.preventDefault();
262
+ if (config.player.paused) {
263
+ config.player.play();
264
+ } else {
265
+ config.player.pause();
266
+ }
267
+ }
268
+
269
+ // F for fullscreen
270
+ if (e.code === 'KeyF') {
271
+ e.preventDefault();
272
+ toggleFullscreen();
273
+ }
274
+
275
+ // M for mute
276
+ if (e.code === 'KeyM') {
277
+ e.preventDefault();
278
+ config.player.muted = !config.player.muted;
279
+ }
280
+
281
+ // Arrow keys for seeking
282
+ if (e.code === 'ArrowLeft') {
283
+ e.preventDefault();
284
+ config.player.currentTime -= 5;
285
+ }
286
+
287
+ if (e.code === 'ArrowRight') {
288
+ e.preventDefault();
289
+ config.player.currentTime += 5;
290
+ }
291
+ }
292
+ });
293
+ });
294
+
295
+ // Fetch video data from JSON
296
  async function fetchVideoData() {
297
  try {
298
+ showLoading();
 
299
 
300
+ const response = await fetch(config.databaseUrl);
301
 
302
  if (!response.ok) {
303
+ throw new Error('Failed to fetch video data');
304
  }
305
 
306
  const data = await response.json();
307
 
308
+ if (!data.videos || !Array.isArray(data.videos)) {
309
+ throw new Error('Invalid video data format');
310
  }
311
 
312
+ // Filter out invalid video entries
313
+ config.videos = data.videos.filter(video => {
314
+ return video.title && video.url;
315
+ });
316
+
317
+ config.filteredVideos = [...config.videos];
318
+
319
+ // Calculate total pages
320
+ config.totalPages = Math.ceil(config.filteredVideos.length / config.itemsPerPage);
321
 
322
+ // Render UI
323
+ updateVideoCount();
324
  renderVideoGrid();
325
  renderPagination();
326
 
327
+ hideLoading();
328
+ showVideoGrid();
329
  } catch (error) {
330
  console.error('Error loading video data:', error);
331
+ hideLoading();
332
+ showError();
 
 
 
 
 
 
333
  }
334
  }
335
+
336
+ // Show loading state
337
+ function showLoading() {
338
+ elements.loadingState.classList.remove('hidden');
339
+ elements.errorState.classList.add('hidden');
340
+ elements.videoGrid.classList.add('hidden');
341
+ }
342
+
343
+ // Hide loading state
344
+ function hideLoading() {
345
+ elements.loadingState.classList.add('hidden');
346
+ }
347
+
348
+ // Show error state
349
+ function showError() {
350
+ elements.errorState.classList.remove('hidden');
351
+ }
352
+
353
+ // Show video grid
354
+ function showVideoGrid() {
355
+ elements.videoGrid.classList.remove('hidden');
356
+ }
357
+
358
+ // Handle search input
359
+ function handleSearch() {
360
+ const searchTerm = elements.searchInput.value.toLowerCase();
361
+
362
+ if (searchTerm === '') {
363
+ config.filteredVideos = [...config.videos];
364
+ } else {
365
+ config.filteredVideos = config.videos.filter(video =>
366
+ video.title.toLowerCase().includes(searchTerm)
367
+ );
368
+ }
369
+
370
+ // Reset to first page
371
+ config.currentPage = 1;
372
+ config.totalPages = Math.ceil(config.filteredVideos.length / config.itemsPerPage);
373
+
374
+ updateVideoCount();
375
+ renderVideoGrid();
376
+ renderPagination();
377
+ }
378
+
379
+ // Handle items per page change
380
+ function handleItemsPerPageChange() {
381
+ config.itemsPerPage = parseInt(elements.itemsPerPage.value);
382
+ config.currentPage = 1;
383
+ config.totalPages = Math.ceil(config.filteredVideos.length / config.itemsPerPage);
384
+
385
+ renderVideoGrid();
386
+ renderPagination();
387
+ }
388
+
389
+ // Update video count display
390
+ function updateVideoCount() {
391
+ elements.videoCount.textContent = config.filteredVideos.length;
392
+ }
393
+
394
+ // Render video grid
395
  function renderVideoGrid() {
396
+ elements.videoContainer.innerHTML = '';
 
 
397
 
398
+ const startIndex = (config.currentPage - 1) * config.itemsPerPage;
399
+ const endIndex = startIndex + config.itemsPerPage;
400
+ const videosToDisplay = config.filteredVideos.slice(startIndex, endIndex);
401
 
402
  if (videosToDisplay.length === 0) {
403
+ elements.videoContainer.innerHTML = `
404
+ <div class="col-span-full text-center py-10">
405
+ <i class="fas fa-video-slash text-4xl text-gray-400 mb-4"></i>
406
+ <h3 class="text-xl font-medium text-gray-600">No videos found</h3>
407
+ <p class="text-gray-500 mt-2">Try adjusting your search or filters</p>
408
  </div>
409
  `;
410
  return;
411
  }
412
 
413
+ videosToDisplay.forEach(video => {
414
  const videoCard = document.createElement('div');
415
+ videoCard.className = 'bg-white rounded-xl shadow-md overflow-hidden transition-transform duration-200 hover:shadow-lg hover:-translate-y-1';
 
 
 
 
416
  videoCard.innerHTML = `
417
+ <div class="relative cursor-pointer" onclick="playVideo('${video.url}', '${escapeHtml(video.title)}')">
418
+ <div class="aspect-w-16 aspect-h-9 bg-gray-200 relative">
419
+ <div class="absolute inset-0 flex items-center justify-center">
420
+ <i class="fas fa-play text-white text-4xl opacity-70"></i>
421
+ </div>
422
+ <img src="${video.thumbnail || 'https://via.placeholder.com/320x180?text=No+Thumbnail'}"
423
+ alt="${escapeHtml(video.title)}"
424
+ class="w-full h-full object-cover">
425
+ </div>
426
+ <div class="p-4">
427
+ <h3 class="font-medium text-gray-800 truncate">${escapeHtml(video.title)}</h3>
428
+ </div>
429
+ <div class="absolute inset-0 bg-black bg-opacity-0 hover:bg-opacity-20 transition duration-200"></div>
430
  </div>
431
  `;
432
+ elements.videoContainer.appendChild(videoCard);
 
 
433
  });
434
  }
435
+
436
  // Render pagination controls
437
  function renderPagination() {
438
+ elements.pageNumbers.innerHTML = '';
439
 
440
+ // Disable prev/next buttons if on first/last page
441
+ elements.prevPage.disabled = config.currentPage === 1;
442
+ elements.nextPage.disabled = config.currentPage === config.totalPages;
443
 
444
+ // Always show first page
445
+ addPageButton(1);
 
446
 
447
+ // Show ellipsis if needed before current page
448
+ if (config.currentPage > 3) {
449
+ addEllipsis();
 
 
 
 
 
 
450
  }
451
 
452
+ // Show pages around current page
453
+ const startPage = Math.max(2, config.currentPage - 1);
454
+ const endPage = Math.min(config.totalPages - 1, config.currentPage + 1);
 
 
 
 
 
 
 
455
 
456
+ for (let i = startPage; i <= endPage; i++) {
457
+ addPageButton(i);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
458
  }
459
 
460
+ // Show ellipsis if needed after current page
461
+ if (config.currentPage < config.totalPages - 2) {
462
+ addEllipsis();
 
 
 
 
 
 
463
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
464
 
465
+ // Always show last page if different from first
466
+ if (config.totalPages > 1) {
467
+ addPageButton(config.totalPages);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
  }
470
+
471
+ // Helper to add a page button
472
+ function addPageButton(pageNumber) {
473
+ const button = document.createElement('button');
474
+ button.className = `px-3 py-1 rounded border ${config.currentPage === pageNumber ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-700 hover:bg-gray-50'}`;
475
+ button.textContent = pageNumber;
476
+ button.addEventListener('click', () => goToPage(pageNumber));
477
+ elements.pageNumbers.appendChild(button);
478
+ }
479
+
480
+ // Helper to add ellipsis
481
+ function addEll
482
  </html>