Subham9126 commited on
Commit
f848982
·
verified ·
1 Parent(s): 2f160a3

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +866 -87
index.html CHANGED
@@ -4,130 +4,909 @@
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>HF Video Library</title>
 
 
7
  <style>
8
- /* Custom CSS for the application */
9
- #search-bar {
10
- margin: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  text-align: center;
12
  }
13
- #search-input {
14
- padding: 10px;
15
- width: 300px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  }
17
- #video-grid {
 
18
  display: grid;
19
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
20
  gap: 20px;
21
- padding: 20px;
22
  }
 
23
  .video-card {
24
- border: 1px solid #ccc;
25
- padding: 10px;
26
- text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  cursor: pointer;
 
 
28
  }
29
- #video-modal {
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  position: fixed;
31
  top: 0;
32
  left: 0;
33
- width: 100%;
34
- height: 100%;
35
- background: rgba(0,0,0,0.8);
36
  display: flex;
37
- justify-content: center;
38
  align-items: center;
 
 
 
 
 
 
 
 
 
 
 
39
  }
40
- #player {
41
- width: 80%;
42
- height: 80%;
 
 
 
 
 
 
43
  }
44
- #close-modal {
 
45
  position: absolute;
46
- top: 20px;
47
- right: 20px;
48
- background: white;
 
 
49
  border: none;
50
- padding: 10px;
 
 
 
 
 
51
  cursor: pointer;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  }
53
  </style>
54
  </head>
55
  <body>
56
- <div id="search-bar">
57
- <input type="text" id="search-input" placeholder="Search videos...">
58
- </div>
59
- <div id="video-grid">
60
- <!-- Videos will be rendered here -->
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  </div>
62
- <div id="video-modal" style="display:none;">
63
- <div id="player"></div>
64
- <button id="close-modal">Close</button>
 
 
 
65
  </div>
66
- <script src="[invalid url, do not cite]
67
- <link rel="stylesheet" href="[invalid url, do not cite] />
 
 
68
  <script>
69
- // Custom JavaScript for the application
70
- let videos = [];
71
- let player;
72
-
73
- // Fetch video data from database.json
74
- fetch('database.json')
75
- .then(response => response.json())
76
- .then(data => {
77
- videos = data.videos;
78
- renderVideos(videos);
79
- })
80
- .catch(error => console.error('Error loading database.json', error));
81
-
82
- // Render videos in the grid
83
- function renderVideos(videosToRender) {
84
- const grid = document.getElementById('video-grid');
85
- grid.innerHTML = '';
86
- videosToRender.forEach(video => {
87
- const card = document.createElement('div');
88
- card.className = 'video-card';
89
- card.innerHTML = `<h3>${video.title}</h3><p>Click to play</p>`;
90
- card.onclick = () => playVideo(video.url, video.title);
91
- grid.appendChild(card);
92
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  }
94
 
95
- // Handle search functionality
96
- const searchInput = document.getElementById('search-input');
97
- searchInput.addEventListener('input', () => {
98
- const query = searchInput.value.toLowerCase();
99
- const filteredVideos = videos.filter(video => video.title.toLowerCase().includes(query));
100
- renderVideos(filteredVideos);
101
- });
102
 
103
- // Play video in modal
104
- function playVideo(url, title) {
105
- if (player) {
106
- player.destroy();
107
- }
108
- const modal = document.getElementById('video-modal');
109
- modal.style.display = 'flex';
110
- const playerElement = document.getElementById('player');
111
- playerElement.innerHTML = `<video id="plyr-player" playsinline controls><source src="${url}" type="video/mp4"></video>`;
112
- player = new Plyr('#plyr-player');
113
  }
114
 
115
- // Close modal functionality
116
- document.getElementById('close-modal').addEventListener('click', closeModal);
117
- document.getElementById('video-modal').addEventListener('click', (e) => {
118
- if (e.target === document.getElementById('video-modal')) {
119
- closeModal();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  }
121
- });
 
 
 
 
 
 
122
 
123
  function closeModal() {
124
- const modal = document.getElementById('video-modal');
125
- modal.style.display = 'none';
126
- if (player) {
127
- player.destroy();
128
- player = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  }
 
 
 
 
 
 
130
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  </script>
132
  </body>
133
  </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
+ <!-- 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>