Subham9126 commited on
Commit
22fa71e
·
verified ·
1 Parent(s): a08ce17

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +41 -893
index.html CHANGED
@@ -3,910 +3,58 @@
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 CSS -->
8
- <link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css" />
9
- <style>
10
- :root {
11
- --primary-color: #6366f1;
12
- --primary-hover: #4f46e5;
13
- --text-color: #1f2937;
14
- --bg-color: #f9fafb;
15
- --card-bg: #ffffff;
16
- --border-color: #e5e7eb;
17
- --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
18
- }
19
-
20
- * {
21
- margin: 0;
22
- padding: 0;
23
- box-sizing: border-box;
24
- }
25
-
26
- body {
27
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
28
- background-color: var(--bg-color);
29
- color: var(--text-color);
30
- line-height: 1.5;
31
- padding: 20px;
32
- }
33
-
34
- .container {
35
- max-width: 1200px;
36
- margin: 0 auto;
37
- }
38
-
39
- header {
40
- display: flex;
41
- flex-direction: column;
42
- gap: 20px;
43
- margin-bottom: 30px;
44
- }
45
-
46
- h1 {
47
- font-size: 2.5rem;
48
- font-weight: 700;
49
- color: var(--primary-color);
50
- text-align: center;
51
- }
52
-
53
- .search-box {
54
- display: flex;
55
- max-width: 500px;
56
- margin: 0 auto;
57
- width: 100%;
58
- }
59
-
60
- .search-input {
61
- flex: 1;
62
- padding: 12px 16px;
63
- font-size: 1rem;
64
- border: 1px solid var(--border-color);
65
- border-radius: 6px;
66
- outline: none;
67
- transition: border-color 0.2s;
68
- }
69
-
70
- .search-input:focus {
71
- border-color: var(--primary-color);
72
- box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
73
- }
74
-
75
- .video-grid {
76
- display: grid;
77
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
78
- gap: 20px;
79
- margin-bottom: 30px;
80
- }
81
-
82
- .video-card {
83
- background-color: var(--card-bg);
84
- border-radius: 8px;
85
- overflow: hidden;
86
- box-shadow: var(--shadow);
87
- transition: transform 0.2s, box-shadow 0.2s;
88
- cursor: pointer;
89
- height: 100%;
90
- display: flex;
91
- flex-direction: column;
92
- }
93
-
94
- .video-card:hover {
95
- transform: translateY(-5px);
96
- box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
97
- }
98
-
99
- .video-thumbnail {
100
- width: 100%;
101
- aspect-ratio: 16 / 9;
102
- background-color: #eee;
103
- display: flex;
104
- align-items: center;
105
- justify-content: center;
106
- color: #888;
107
- }
108
-
109
- .video-thumbnail svg {
110
- width: 48px;
111
- height: 48px;
112
- color: var(--primary-color);
113
- }
114
-
115
- .video-info {
116
- padding: 16px;
117
- flex-grow: 1;
118
- display: flex;
119
- flex-direction: column;
120
- }
121
-
122
- .video-title {
123
- font-size: 1rem;
124
- font-weight: 600;
125
- margin-bottom: 8px;
126
- display: -webkit-box;
127
- -webkit-line-clamp: 2;
128
- -webkit-box-orient: vertical;
129
- overflow: hidden;
130
- }
131
-
132
- .pagination {
133
- display: flex;
134
- justify-content: center;
135
- gap: 8px;
136
- margin-top: 30px;
137
- flex-wrap: wrap;
138
- }
139
-
140
- .page-btn {
141
- padding: 8px 12px;
142
- background-color: var(--card-bg);
143
- border: 1px solid var(--border-color);
144
- border-radius: 4px;
145
- cursor: pointer;
146
- font-size: 0.9rem;
147
- transition: all 0.2s;
148
- }
149
-
150
- .page-btn:hover {
151
- background-color: var(--primary-hover);
152
- color: white;
153
- border-color: var(--primary-hover);
154
- }
155
-
156
- .page-btn.active {
157
- background-color: var(--primary-color);
158
- color: white;
159
- border-color: var(--primary-color);
160
- }
161
-
162
- .modal-overlay {
163
- position: fixed;
164
- top: 0;
165
- left: 0;
166
- right: 0;
167
- bottom: 0;
168
- background-color: rgba(0, 0, 0, 0.75);
169
- display: flex;
170
- align-items: center;
171
- justify-content: center;
172
- z-index: 1000;
173
- padding: 20px;
174
- opacity: 0;
175
- visibility: hidden;
176
- transition: opacity 0.3s, visibility 0.3s;
177
- }
178
-
179
- .modal-overlay.active {
180
- opacity: 1;
181
- visibility: visible;
182
- }
183
-
184
- .modal-content {
185
- width: 100%;
186
- max-width: 900px;
187
- background-color: var(--card-bg);
188
- border-radius: 8px;
189
- overflow: hidden;
190
- position: relative;
191
- padding-top: 56.25%; /* 16:9 aspect ratio */
192
- }
193
-
194
- .modal-close {
195
- position: absolute;
196
- top: 10px;
197
- right: 10px;
198
- width: 36px;
199
- height: 36px;
200
- background-color: rgba(0, 0, 0, 0.5);
201
- border: none;
202
- border-radius: 50%;
203
- color: white;
204
- font-size: 20px;
205
- display: flex;
206
- align-items: center;
207
- justify-content: center;
208
- cursor: pointer;
209
- z-index: 1010;
210
- transition: background-color 0.2s;
211
- }
212
-
213
- .modal-close:hover {
214
- background-color: rgba(0, 0, 0, 0.7);
215
- }
216
-
217
- .video-container {
218
- position: absolute;
219
- top: 0;
220
- left: 0;
221
- width: 100%;
222
- height: 100%;
223
- }
224
-
225
- .video-player {
226
- width: 100%;
227
- height: 100%;
228
- }
229
-
230
- .loader {
231
- display: flex;
232
- flex-direction: column;
233
- align-items: center;
234
- justify-content: center;
235
- min-height: 200px;
236
- text-align: center;
237
- }
238
-
239
- .spinner {
240
- width: 50px;
241
- height: 50px;
242
- border: 5px solid rgba(99, 102, 241, 0.3);
243
- border-radius: 50%;
244
- border-top-color: var(--primary-color);
245
- animation: spin 1s ease-in-out infinite;
246
- margin-bottom: 20px;
247
- }
248
-
249
- @keyframes spin {
250
- to { transform: rotate(360deg); }
251
- }
252
-
253
- .error-message {
254
- color: #ef4444;
255
- padding: 16px;
256
- text-align: center;
257
- background-color: #fee2e2;
258
- border-radius: 6px;
259
- margin-bottom: 20px;
260
- border: 1px solid #fecaca;
261
- }
262
-
263
- @media (max-width: 640px) {
264
- .video-grid {
265
- grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
266
- gap: 16px;
267
- }
268
-
269
- h1 {
270
- font-size: 1.75rem;
271
- }
272
-
273
- .search-input {
274
- padding: 10px 12px;
275
- }
276
-
277
- .modal-content {
278
- max-width: 100%;
279
- }
280
- }
281
-
282
- /* Pagination with jump-to-page input for large collections */
283
- .pagination-jump {
284
- display: flex;
285
- align-items: center;
286
- gap: 8px;
287
- margin-left: 8px;
288
- }
289
-
290
- .jump-input {
291
- width: 60px;
292
- padding: 8px;
293
- border: 1px solid var(--border-color);
294
- border-radius: 4px;
295
- text-align: center;
296
- }
297
-
298
- .no-results {
299
- grid-column: 1 / -1;
300
- text-align: center;
301
- padding: 40px;
302
- background-color: var(--card-bg);
303
- border-radius: 8px;
304
- box-shadow: var(--shadow);
305
- }
306
-
307
- /* Improved loading states */
308
- .video-card-skeleton {
309
- background-color: var(--card-bg);
310
- border-radius: 8px;
311
- overflow: hidden;
312
- box-shadow: var(--shadow);
313
- height: 100%;
314
- display: flex;
315
- flex-direction: column;
316
- }
317
-
318
- .skeleton-thumbnail {
319
- width: 100%;
320
- aspect-ratio: 16 / 9;
321
- background: linear-gradient(90deg, #f0f0f0 25%, #f8f8f8 50%, #f0f0f0 75%);
322
- background-size: 200% 100%;
323
- animation: shimmer 1.5s infinite;
324
- }
325
-
326
- .skeleton-info {
327
- padding: 16px;
328
- }
329
-
330
- .skeleton-title {
331
- height: 20px;
332
- margin-bottom: 8px;
333
- background: linear-gradient(90deg, #f0f0f0 25%, #f8f8f8 50%, #f0f0f0 75%);
334
- background-size: 200% 100%;
335
- animation: shimmer 1.5s infinite;
336
- border-radius: 4px;
337
- }
338
-
339
- @keyframes shimmer {
340
- 0% { background-position: -200% 0; }
341
- 100% { background-position: 200% 0; }
342
- }
343
- </style>
344
  </head>
345
  <body>
346
  <div class="container">
347
  <header>
348
- <h1>HF Video Library</h1>
349
- <div class="search-box">
350
- <input type="text" class="search-input" placeholder="Search videos..." id="searchInput">
 
351
  </div>
352
  </header>
353
 
354
- <div id="errorContainer"></div>
355
-
356
- <div id="loadingIndicator" class="loader">
357
- <div class="spinner"></div>
358
- <p>Loading videos...</p>
359
- </div>
360
-
361
- <div id="videoGrid" class="video-grid" style="display: none;"></div>
362
-
363
- <div id="pagination" class="pagination"></div>
364
- </div>
365
-
366
- <div class="modal-overlay" id="videoModal">
367
- <button class="modal-close" id="closeModal">✕</button>
368
- <div class="modal-content">
369
- <div class="video-container" id="videoContainer"></div>
370
- </div>
371
- </div>
372
-
373
- <!-- Plyr.js -->
374
- <script src="https://cdn.plyr.io/3.7.8/plyr.polyfilled.js"></script>
375
-
376
- <script>
377
- // Configuration
378
- const CONFIG = {
379
- videosPerPage: 12,
380
- dataSource: 'database.json',
381
- playerOptions: {
382
- controls: [
383
- 'play-large', 'play', 'progress', 'current-time', 'mute', 'volume',
384
- 'captions', 'settings', 'pip', 'airplay', 'fullscreen'
385
- ],
386
- settings: ['captions', 'quality', 'speed'],
387
- keyboard: { focused: true, global: false },
388
- resetOnEnd: true
389
- }
390
- };
391
-
392
- // State management
393
- const STATE = {
394
- videos: [],
395
- filteredVideos: [],
396
- currentPage: 1,
397
- player: null,
398
- videoElement: null,
399
- searchQuery: '',
400
- loading: true,
401
- error: null
402
- };
403
-
404
- // DOM Elements
405
- const DOM = {
406
- videoGrid: document.getElementById('videoGrid'),
407
- loadingIndicator: document.getElementById('loadingIndicator'),
408
- pagination: document.getElementById('pagination'),
409
- videoModal: document.getElementById('videoModal'),
410
- closeModal: document.getElementById('closeModal'),
411
- videoContainer: document.getElementById('videoContainer'),
412
- searchInput: document.getElementById('searchInput'),
413
- errorContainer: document.getElementById('errorContainer')
414
- };
415
-
416
- // Helper Functions
417
- function showError(message) {
418
- STATE.error = message;
419
- DOM.errorContainer.innerHTML = `
420
- <div class="error-message">
421
- <strong>Error:</strong> ${message}
422
- </div>
423
- `;
424
- DOM.loadingIndicator.style.display = 'none';
425
- }
426
-
427
- function clearError() {
428
- STATE.error = null;
429
- DOM.errorContainer.innerHTML = '';
430
- }
431
-
432
- function createVideoElement() {
433
- // Completely create a new video element each time
434
- const videoElement = document.createElement('video');
435
- videoElement.className = 'video-player';
436
- videoElement.controls = true;
437
- videoElement.preload = 'auto';
438
- videoElement.crossOrigin = 'anonymous'; // For potential subtitles
439
-
440
- return videoElement;
441
- }
442
-
443
- // Event Handlers
444
- function handleVideoClick(index) {
445
- const video = STATE.filteredVideos[index];
446
- if (!video) return;
447
-
448
- openModal();
449
- playVideo(video);
450
- }
451
-
452
- function handleSearchInput(event) {
453
- STATE.searchQuery = event.target.value.trim().toLowerCase();
454
- filterVideos();
455
- STATE.currentPage = 1; // Reset to first page when searching
456
- renderVideos();
457
- renderPagination();
458
- }
459
-
460
- function handlePageClick(page) {
461
- if (page === STATE.currentPage) return;
462
- STATE.currentPage = page;
463
- renderVideos();
464
- renderPagination();
465
- window.scrollTo({ top: 0, behavior: 'smooth' });
466
- }
467
-
468
- function handleCloseModal() {
469
- closeModal();
470
- cleanupPlayer();
471
- }
472
-
473
- function handleKeyDown(event) {
474
- if (event.key === 'Escape' && DOM.videoModal.classList.contains('active')) {
475
- handleCloseModal();
476
- }
477
- }
478
-
479
- // Modal Functions
480
- function openModal() {
481
- DOM.videoModal.classList.add('active');
482
- document.body.style.overflow = 'hidden'; // Prevent scrolling
483
- }
484
-
485
- function closeModal() {
486
- DOM.videoModal.classList.remove('active');
487
- document.body.style.overflow = '';
488
- }
489
-
490
- // Video Player Functions
491
- function playVideo(video) {
492
- // Always clean up previous player first (important)
493
- cleanupPlayer();
494
-
495
- // Clear the video container completely
496
- DOM.videoContainer.innerHTML = '';
497
-
498
- // Create a fresh video element for each playback
499
- STATE.videoElement = createVideoElement();
500
- DOM.videoContainer.appendChild(STATE.videoElement);
501
-
502
- // Prepare the video URL
503
- let videoUrl = video.url;
504
-
505
- // Add download parameter to Hugging Face URLs
506
- if (videoUrl.includes('huggingface.co')) {
507
- videoUrl = addQueryParam(videoUrl, 'download', 'true');
508
- }
509
-
510
- // Set the source
511
- STATE.videoElement.src = videoUrl;
512
-
513
- try {
514
- // Initialize the player with the new element
515
- STATE.player = new Plyr(STATE.videoElement, CONFIG.playerOptions);
516
-
517
- // Set up event listeners
518
- STATE.player.on('error', (event) => {
519
- console.error('Plyr error:', event);
520
-
521
- // Check if there's a detailed error message
522
- let errorMsg = 'Unable to play this video.';
523
- if (STATE.videoElement.error) {
524
- switch (STATE.videoElement.error.code) {
525
- case 1: errorMsg += ' The video file was not found.'; break;
526
- case 2: errorMsg += ' Network error occurred.'; break;
527
- case 3: errorMsg += ' Video decoding failed. The format may not be supported by your browser.'; break;
528
- case 4: errorMsg += ' Video is not playable due to permissions or content protection.'; break;
529
- }
530
- }
531
-
532
- // Display error in the player
533
- const errorOverlay = document.createElement('div');
534
- errorOverlay.style.position = 'absolute';
535
- errorOverlay.style.top = '0';
536
- errorOverlay.style.left = '0';
537
- errorOverlay.style.width = '100%';
538
- errorOverlay.style.height = '100%';
539
- errorOverlay.style.backgroundColor = 'rgba(0,0,0,0.7)';
540
- errorOverlay.style.color = 'white';
541
- errorOverlay.style.display = 'flex';
542
- errorOverlay.style.alignItems = 'center';
543
- errorOverlay.style.justifyContent = 'center';
544
- errorOverlay.style.textAlign = 'center';
545
- errorOverlay.style.padding = '20px';
546
- errorOverlay.innerHTML = `<div>${errorMsg}</div>`;
547
-
548
- DOM.videoContainer.appendChild(errorOverlay);
549
- });
550
-
551
- // Auto-play
552
- STATE.player.play().catch(error => {
553
- console.warn('Auto-play was prevented:', error);
554
- // This is fine, as many browsers block autoplay
555
- });
556
-
557
- } catch (error) {
558
- console.error('Failed to initialize player:', error);
559
- DOM.videoContainer.innerHTML = `<div style="padding: 20px; text-align: center;">
560
- Failed to initialize video player. Please try again.
561
- </div>`;
562
- }
563
- }
564
-
565
- function cleanupPlayer() {
566
- if (STATE.player) {
567
- // Force stop any playback
568
- try {
569
- if (!STATE.player.paused) {
570
- STATE.player.pause();
571
- }
572
-
573
- // Destroy the player completely
574
- STATE.player.destroy();
575
- } catch (e) {
576
- console.warn('Error during player cleanup:', e);
577
- }
578
-
579
- STATE.player = null;
580
- }
581
-
582
- // Ensure any lingering video element is properly cleaned up
583
- if (STATE.videoElement) {
584
- try {
585
- // Force stop
586
- STATE.videoElement.pause();
587
-
588
- // Clear all sources explicitly
589
- STATE.videoElement.removeAttribute('src');
590
- STATE.videoElement.load();
591
- } catch (e) {
592
- console.warn('Error clearing video element:', e);
593
- }
594
-
595
- STATE.videoElement = null;
596
- }
597
-
598
- // Extra safety - remove all content
599
- DOM.videoContainer.innerHTML = '';
600
- }
601
-
602
- // Data Functions
603
- async function fetchVideoData() {
604
- try {
605
- clearError();
606
- STATE.loading = true;
607
- renderLoadingState();
608
-
609
- const response = await fetch(CONFIG.dataSource);
610
-
611
- if (!response.ok) {
612
- throw new Error(`Failed to load video data (Status: ${response.status})`);
613
- }
614
-
615
- const data = await response.json();
616
-
617
- // Validate data structure
618
- if (!data || !Array.isArray(data.videos)) {
619
- throw new Error('Invalid data format: Expected {"videos": [...]}');
620
- }
621
-
622
- // Filter out invalid entries
623
- STATE.videos = data.videos.filter(video => {
624
- const isValid = video && typeof video.title === 'string' &&
625
- typeof video.url === 'string' && video.url.trim() !== '';
626
-
627
- if (!isValid) {
628
- console.warn('Skipping invalid video entry:', video);
629
- }
630
-
631
- return isValid;
632
- });
633
-
634
- // Initial filtering
635
- filterVideos();
636
-
637
- // Render the UI
638
- STATE.loading = false;
639
- renderUI();
640
-
641
- } catch (error) {
642
- console.error('Error fetching video data:', error);
643
- showError(error.message || 'Failed to load video data. Please try again later.');
644
- }
645
- }
646
-
647
- function filterVideos() {
648
- const query = STATE.searchQuery;
649
-
650
- if (!query) {
651
- STATE.filteredVideos = [...STATE.videos];
652
- return;
653
- }
654
-
655
- STATE.filteredVideos = STATE.videos.filter(video =>
656
- video.title.toLowerCase().includes(query)
657
- );
658
- }
659
-
660
- // Rendering Functions
661
- function renderUI() {
662
- if (STATE.loading) {
663
- renderLoadingState();
664
- return;
665
- }
666
-
667
- DOM.loadingIndicator.style.display = 'none';
668
- DOM.videoGrid.style.display = 'grid';
669
-
670
- renderVideos();
671
- renderPagination();
672
- }
673
-
674
- function renderLoadingState() {
675
- DOM.loadingIndicator.style.display = 'flex';
676
- DOM.videoGrid.style.display = 'none';
677
- DOM.pagination.innerHTML = '';
678
-
679
- // Show skeleton loading for better UX
680
- DOM.videoGrid.innerHTML = Array(CONFIG.videosPerPage)
681
- .fill()
682
- .map(() => `
683
- <div class="video-card-skeleton">
684
- <div class="skeleton-thumbnail"></div>
685
- <div class="skeleton-info">
686
- <div class="skeleton-title"></div>
687
- <div class="skeleton-title" style="width: 70%"></div>
688
- </div>
689
- </div>
690
- `)
691
- .join('');
692
- }
693
-
694
- function renderVideos() {
695
- if (STATE.filteredVideos.length === 0) {
696
- DOM.videoGrid.innerHTML = `
697
- <div class="no-results">
698
- <h3>No videos found</h3>
699
- <p>Try adjusting your search query</p>
700
  </div>
701
- `;
702
- DOM.pagination.innerHTML = '';
703
- return;
704
- }
705
-
706
- const startIndex = (STATE.currentPage - 1) * CONFIG.videosPerPage;
707
- const endIndex = startIndex + CONFIG.videosPerPage;
708
- const pageVideos = STATE.filteredVideos.slice(startIndex, endIndex);
709
-
710
- DOM.videoGrid.innerHTML = pageVideos.map((video, index) => `
711
- <div class="video-card" onclick="handleVideoClick(${startIndex + index})">
712
- <div class="video-thumbnail">
713
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
714
- <polygon points="5 3 19 12 5 21 5 3"></polygon>
715
- </svg>
716
- </div>
717
- <div class="video-info">
718
- <h3 class="video-title">${escapeHTML(video.title)}</h3>
719
  </div>
720
  </div>
721
- `).join('');
722
- }
723
-
724
- function renderPagination() {
725
- const totalPages = Math.ceil(STATE.filteredVideos.length / CONFIG.videosPerPage);
726
-
727
- if (totalPages <= 1) {
728
- DOM.pagination.innerHTML = '';
729
- return;
730
- }
731
-
732
- // Determine which page numbers to show
733
- let pageNumbers = [];
734
- const maxVisiblePages = 5;
735
-
736
- if (totalPages <= maxVisiblePages) {
737
- // Show all pages
738
- pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1);
739
- } else {
740
- // Always include first and last page
741
- pageNumbers.push(1);
742
-
743
- // Calculate middle pages
744
- let startPage = Math.max(2, STATE.currentPage - 1);
745
- let endPage = Math.min(totalPages - 1, STATE.currentPage + 1);
746
-
747
- // Adjust if we're near the start or end
748
- if (STATE.currentPage <= 2) {
749
- endPage = 4;
750
- } else if (STATE.currentPage >= totalPages - 1) {
751
- startPage = totalPages - 3;
752
- }
753
-
754
- // Add ellipsis if needed
755
- if (startPage > 2) {
756
- pageNumbers.push('...');
757
- }
758
-
759
- // Add middle pages
760
- for (let i = startPage; i <= endPage; i++) {
761
- pageNumbers.push(i);
762
- }
763
-
764
- // Add ellipsis if needed
765
- if (endPage < totalPages - 1) {
766
- pageNumbers.push('...');
767
- }
768
-
769
- // Add last page if not already added
770
- if (totalPages > 1) {
771
- pageNumbers.push(totalPages);
772
- }
773
- }
774
-
775
- // Generate pagination HTML
776
- const paginationHTML = pageNumbers.map(page => {
777
- if (page === '...') {
778
- return `<span class="page-btn">...</span>`;
779
- }
780
- return `
781
- <button class="page-btn ${page === STATE.currentPage ? 'active' : ''}"
782
- onclick="handlePageClick(${page})">
783
- ${page}
784
- </button>
785
- `;
786
- }).join('');
787
-
788
- // Add jump-to-page for large collections
789
- const jumpToPageInput = totalPages > maxVisiblePages ? `
790
- <div class="pagination-jump">
791
- <span>Go to:</span>
792
- <input type="number" class="jump-input" id="jumpToPage"
793
- min="1" max="${totalPages}" value="${STATE.currentPage}">
794
- <button class="page-btn" onclick="jumpToPage()">Go</button>
795
  </div>
796
- ` : '';
797
-
798
- DOM.pagination.innerHTML = paginationHTML + jumpToPageInput;
799
-
800
- // Add event listener to jump input
801
- const jumpInput = document.getElementById('jumpToPage');
802
- if (jumpInput) {
803
- jumpInput.addEventListener('keydown', (e) => {
804
- if (e.key === 'Enter') {
805
- jumpToPage();
806
- }
807
- });
808
- }
809
- }
810
-
811
- // Utility Functions
812
- function escapeHTML(str) {
813
- const div = document.createElement('div');
814
- div.textContent = str;
815
- return div.innerHTML;
816
- }
817
 
818
- function addQueryParam(url, param, value) {
819
- const separator = url.includes('?') ? '&' : '?';
820
- return `${url}${separator}${param}=${encodeURIComponent(value)}`;
821
- }
822
-
823
- function jumpToPage() {
824
- const input = document.getElementById('jumpToPage');
825
- if (!input) return;
826
-
827
- let page = parseInt(input.value);
828
- const totalPages = Math.ceil(STATE.filteredVideos.length / CONFIG.videosPerPage);
829
-
830
- // Validate input
831
- if (isNaN(page) || page < 1) {
832
- page = 1;
833
- } else if (page > totalPages) {
834
- page = totalPages;
835
- }
836
-
837
- handlePageClick(page);
838
- }
839
-
840
- // Mobile double-tap handlers
841
- function setupDoubleTapHandlers() {
842
- let lastTap = 0;
843
- let touchTimeout;
844
-
845
- document.addEventListener('touchend', (e) => {
846
- // Only handle if modal is active and player exists
847
- if (!DOM.videoModal.classList.contains('active') || !STATE.player) return;
848
-
849
- const currentTime = new Date().getTime();
850
- const tapLength = currentTime - lastTap;
851
-
852
- clearTimeout(touchTimeout);
853
-
854
- if (tapLength < 500 && tapLength > 0) {
855
- // Double tap detected
856
- e.preventDefault();
857
-
858
- // Determine which side of the screen was tapped
859
- const screenWidth = window.innerWidth;
860
- const touchX = e.changedTouches[0].clientX;
861
-
862
- if (touchX < screenWidth / 4) {
863
- // Left side - seek backward 10 seconds
864
- STATE.player.currentTime -= 10;
865
- } else if (touchX > (screenWidth * 3) / 4) {
866
- // Right side - seek forward 10 seconds
867
- STATE.player.currentTime += 10;
868
- } else {
869
- // Middle - toggle play/pause
870
- if (STATE.player.paused) {
871
- STATE.player.play();
872
- } else {
873
- STATE.player.pause();
874
- }
875
- }
876
- } else {
877
- // Single tap - wait to see if double tap follows
878
- touchTimeout = setTimeout(() => {
879
- // Single tap handling if needed
880
- }, 300);
881
- lastTap = currentTime;
882
- }
883
- });
884
- }
885
-
886
- // Initialization and Event Listeners
887
- document.addEventListener('DOMContentLoaded', () => {
888
- // Set up event listeners
889
- DOM.searchInput.addEventListener('input', handleSearchInput);
890
- DOM.closeModal.addEventListener('click', handleCloseModal);
891
- DOM.videoModal.addEventListener('click', (event) => {
892
- // Close modal if clicking outside the content
893
- if (event.target === DOM.videoModal) {
894
- handleCloseModal();
895
- }
896
- });
897
- document.addEventListener('keydown', handleKeyDown);
898
-
899
- // Set up mobile-specific handlers
900
- setupDoubleTapHandlers();
901
-
902
- // Fetch data
903
- fetchVideoData();
904
- });
905
 
906
- // Make handlers globally accessible for onclick attributes
907
- window.handleVideoClick = handleVideoClick;
908
- window.handlePageClick = handlePageClick;
909
- window.jumpToPage = jumpToPage;
910
- </script>
911
  </body>
912
- </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Video Streaming Dashboard</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  </head>
10
  <body>
11
  <div class="container">
12
  <header>
13
+ <h1>My Video Collection</h1>
14
+ <div class="search-container">
15
+ <input type="text" id="searchInput" placeholder="Search videos...">
16
+ <button id="searchButton"><i class="fas fa-search"></i></button>
17
  </div>
18
  </header>
19
 
20
+ <main>
21
+ <section class="video-list-section">
22
+ <h2>Videos</h2>
23
+ <div class="video-list" id="videoList">
24
+ <!-- Videos will be loaded here dynamically -->
25
+ <div class="loading">
26
+ <i class="fas fa-spinner fa-spin"></i>
27
+ <p>Loading videos...</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  </div>
29
+ </div>
30
+ </section>
31
+
32
+ <section class="video-player-section">
33
+ <div class="video-container">
34
+ <video id="videoPlayer" controls>
35
+ <p>Your browser doesn't support HTML5 video.</p>
36
+ </video>
37
+ <div class="no-video-message" id="noVideoMessage">
38
+ <i class="fas fa-film"></i>
39
+ <p>Select a video to play</p>
 
 
 
 
 
 
 
40
  </div>
41
  </div>
42
+ <div class="video-info">
43
+ <h2 id="currentVideoTitle">No video selected</h2>
44
+ <div class="video-controls">
45
+ <button id="fullscreenBtn" title="Fullscreen">
46
+ <i class="fas fa-expand"></i>
47
+ </button>
48
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  </div>
50
+ </section>
51
+ </main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
+ <footer>
54
+ <p>Video Streaming Dashboard &copy; 2025</p>
55
+ </footer>
56
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
+ <script src="script.js"></script>
 
 
 
 
59
  </body>
60
+ </html>