jerrycans commited on
Commit
f71e221
·
verified ·
1 Parent(s): 4465557

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +186 -351
app.py CHANGED
@@ -56,12 +56,6 @@ HTML_PAGE = '''<!DOCTYPE html>
56
  z-index: 100;
57
  border-bottom: 3px solid #fff;
58
  text-transform: uppercase;
59
- animation: fadeInOut 3s ease-in-out infinite;
60
- }
61
-
62
- @keyframes fadeInOut {
63
- 0%, 100% { opacity: 1; }
64
- 50% { opacity: 0.5; }
65
  }
66
 
67
  .classified-banner {
@@ -74,7 +68,6 @@ HTML_PAGE = '''<!DOCTYPE html>
74
  letter-spacing: 4px;
75
  }
76
 
77
- /* ===== LOADING SCREEN ===== */
78
  #loading-screen {
79
  display: flex;
80
  flex-direction: column;
@@ -85,13 +78,13 @@ HTML_PAGE = '''<!DOCTYPE html>
85
  }
86
 
87
  .spinner {
88
- width: 60px;
89
- height: 60px;
90
- border: 4px solid #333;
91
  border-top-color: #fff;
92
  border-radius: 50%;
93
- animation: spin 0.8s linear infinite;
94
- margin-bottom: 30px;
95
  }
96
 
97
  @keyframes spin {
@@ -102,24 +95,21 @@ HTML_PAGE = '''<!DOCTYPE html>
102
  font-size: 14px;
103
  color: #888;
104
  letter-spacing: 3px;
105
- text-transform: uppercase;
106
  }
107
 
108
  .error-message {
109
  color: #f44 !important;
110
  }
111
 
112
- /* ===== GALLERY ===== */
113
  #gallery-screen {
114
  display: none;
115
  padding: 20px;
116
- padding-bottom: 30px;
117
  }
118
 
119
  .gallery-header {
120
  text-align: center;
121
- margin-bottom: 25px;
122
- padding-bottom: 20px;
123
  border-bottom: 1px solid #333;
124
  }
125
 
@@ -127,33 +117,25 @@ HTML_PAGE = '''<!DOCTYPE html>
127
  font-size: 12px;
128
  color: #888;
129
  letter-spacing: 4px;
130
- margin-bottom: 10px;
131
- text-transform: uppercase;
132
  }
133
 
134
  .gallery-count {
135
- font-size: 32px;
136
  font-weight: 800;
137
  color: #fff;
138
- letter-spacing: 4px;
139
  }
140
 
141
  .gallery-count span {
142
  color: #666;
143
- font-size: 16px;
144
- }
145
-
146
- .gallery-subtitle {
147
- font-size: 10px;
148
- color: #555;
149
- margin-top: 10px;
150
- letter-spacing: 2px;
151
  }
152
 
153
  .gallery-grid {
154
  display: grid;
155
- grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
156
- gap: 12px;
157
  }
158
 
159
  .gallery-item {
@@ -163,28 +145,14 @@ HTML_PAGE = '''<!DOCTYPE html>
163
  cursor: pointer;
164
  background: #111;
165
  position: relative;
166
- transition: border-color 0.2s, transform 0.2s;
167
- }
168
-
169
- .gallery-item:hover {
170
- border-color: #fff;
171
- transform: translateY(-3px);
172
- }
173
-
174
- .gallery-item:active {
175
- transform: scale(0.98);
176
  }
177
 
178
  .gallery-item img {
179
  width: 100%;
180
  height: 100%;
181
  object-fit: cover;
182
- opacity: 0;
183
- transition: opacity 0.3s;
184
- }
185
-
186
- .gallery-item img.loaded {
187
- opacity: 1;
188
  }
189
 
190
  .gallery-item .placeholder {
@@ -193,9 +161,8 @@ HTML_PAGE = '''<!DOCTYPE html>
193
  display: flex;
194
  align-items: center;
195
  justify-content: center;
196
- color: #333;
197
  font-size: 10px;
198
- letter-spacing: 1px;
199
  }
200
 
201
  .item-number {
@@ -203,35 +170,19 @@ HTML_PAGE = '''<!DOCTYPE html>
203
  bottom: 0;
204
  left: 0;
205
  right: 0;
206
- background: rgba(0,0,0,0.9);
207
  color: #fff;
208
- padding: 5px;
209
  font-size: 9px;
210
  font-weight: 700;
211
- letter-spacing: 1px;
212
  text-align: center;
213
  }
214
 
215
  #load-more-trigger {
216
- height: 80px;
217
  display: flex;
218
  align-items: center;
219
  justify-content: center;
220
- gap: 15px;
221
- }
222
-
223
- .load-spinner {
224
- width: 20px;
225
- height: 20px;
226
- border: 2px solid #333;
227
- border-top-color: #fff;
228
- border-radius: 50%;
229
- animation: spin 0.8s linear infinite;
230
- display: none;
231
- }
232
-
233
- .load-spinner.active {
234
- display: block;
235
  }
236
 
237
  .load-more-text {
@@ -242,31 +193,25 @@ HTML_PAGE = '''<!DOCTYPE html>
242
 
243
  #scroll-top {
244
  position: fixed;
245
- bottom: 20px;
246
- right: 20px;
247
  background: #000;
248
  border: 2px solid #fff;
249
  color: #fff;
250
- width: 45px;
251
- height: 45px;
252
- font-size: 18px;
253
  cursor: pointer;
254
  z-index: 50;
255
  display: none;
256
- align-items: center;
257
- justify-content: center;
258
- }
259
-
260
- #scroll-top:hover {
261
- background: #fff;
262
- color: #000;
263
  }
264
 
265
  #scroll-top.visible {
266
  display: flex;
 
 
267
  }
268
 
269
- /* ===== LIGHTBOX ===== */
270
  #lightbox {
271
  display: none;
272
  position: fixed;
@@ -284,10 +229,9 @@ HTML_PAGE = '''<!DOCTYPE html>
284
  display: flex;
285
  justify-content: space-between;
286
  align-items: center;
287
- padding: 12px 15px;
288
  background: #000;
289
  border-bottom: 2px solid #fff;
290
- flex-shrink: 0;
291
  }
292
 
293
  .lb-counter {
@@ -299,7 +243,7 @@ HTML_PAGE = '''<!DOCTYPE html>
299
 
300
  .lb-nav {
301
  display: flex;
302
- gap: 10px;
303
  }
304
 
305
  .lb-btn {
@@ -307,26 +251,10 @@ HTML_PAGE = '''<!DOCTYPE html>
307
  border: 2px solid #fff;
308
  color: #fff;
309
  cursor: pointer;
310
- font-size: 16px;
311
- padding: 8px 15px;
312
  font-family: inherit;
313
  font-weight: 700;
314
- -webkit-tap-highlight-color: transparent;
315
- transition: all 0.15s;
316
- }
317
-
318
- .lb-btn:hover {
319
- background: #fff;
320
- color: #000;
321
- }
322
-
323
- .lb-btn:active {
324
- transform: scale(0.95);
325
- }
326
-
327
- .lb-close {
328
- font-size: 18px;
329
- padding: 8px 12px;
330
  }
331
 
332
  .lb-image-container {
@@ -334,7 +262,7 @@ HTML_PAGE = '''<!DOCTYPE html>
334
  display: flex;
335
  align-items: center;
336
  justify-content: center;
337
- padding: 15px;
338
  overflow: hidden;
339
  position: relative;
340
  }
@@ -343,64 +271,35 @@ HTML_PAGE = '''<!DOCTYPE html>
343
  max-width: 100%;
344
  max-height: 100%;
345
  object-fit: contain;
346
- border: 3px solid #fff;
347
- opacity: 0;
348
- transition: opacity 0.2s;
349
- }
350
-
351
- #lightbox-img.loaded {
352
- opacity: 1;
353
  }
354
 
355
- .lb-spinner {
356
  position: absolute;
357
- width: 40px;
358
- height: 40px;
359
- border: 3px solid #333;
360
- border-top-color: #fff;
361
- border-radius: 50%;
362
- animation: spin 0.8s linear infinite;
363
- }
364
-
365
- .lb-spinner.hidden {
366
- display: none;
367
- }
368
-
369
- .lb-footer {
370
- padding: 10px 15px;
371
- text-align: center;
372
- background: #000;
373
- border-top: 1px solid #333;
374
- flex-shrink: 0;
375
- }
376
-
377
- .lb-filename {
378
- font-size: 10px;
379
  color: #666;
380
- letter-spacing: 1px;
381
  }
382
 
383
- /* Mobile */
384
  @media (max-width: 600px) {
385
  #header {
386
- font-size: 18px;
387
- letter-spacing: 6px;
388
- padding: 20px 15px;
389
  }
390
 
391
  .classified-banner {
392
- font-size: 9px;
393
  letter-spacing: 2px;
394
- padding: 6px 10px;
395
  }
396
 
397
  .gallery-grid {
398
  grid-template-columns: repeat(3, 1fr);
399
- gap: 8px;
400
  }
401
 
402
  .gallery-count {
403
- font-size: 26px;
404
  }
405
 
406
  .item-number {
@@ -408,39 +307,9 @@ HTML_PAGE = '''<!DOCTYPE html>
408
  padding: 3px;
409
  }
410
 
411
- .lb-header {
412
- padding: 10px 12px;
413
- }
414
-
415
- .lb-counter {
416
- font-size: 12px;
417
- letter-spacing: 2px;
418
- }
419
-
420
  .lb-btn {
421
- font-size: 14px;
422
- padding: 6px 12px;
423
- }
424
-
425
- .lb-close {
426
- font-size: 16px;
427
- padding: 6px 10px;
428
- }
429
-
430
- .lb-image-container {
431
- padding: 10px;
432
- }
433
-
434
- #lightbox-img {
435
- border-width: 2px;
436
- }
437
-
438
- #scroll-top {
439
- width: 40px;
440
- height: 40px;
441
- font-size: 16px;
442
- bottom: 15px;
443
- right: 15px;
444
  }
445
  }
446
 
@@ -455,262 +324,228 @@ HTML_PAGE = '''<!DOCTYPE html>
455
  <div id="header">EPSTEIN FILES</div>
456
  <div class="classified-banner">★ HERE ARE ALL THE IMAGES ★</div>
457
 
458
- <!-- Loading Screen -->
459
  <div id="loading-screen">
460
  <div class="spinner"></div>
461
  <div class="loading-text" id="loading-text">Loading...</div>
462
  </div>
463
 
464
- <!-- Gallery Screen -->
465
  <div id="gallery-screen">
466
  <div class="gallery-header">
467
- <div class="gallery-title">Images</div>
468
  <div class="gallery-count" id="gallery-count">0 <span>FILES</span></div>
469
- <div class="gallery-subtitle">Click any image to enlarge</div>
470
  </div>
471
  <div class="gallery-grid" id="gallery-grid"></div>
472
  <div id="load-more-trigger">
473
- <div class="load-spinner" id="load-spinner"></div>
474
- <div class="load-more-text" id="load-more-text">SCROLL FOR MORE</div>
475
  </div>
476
  </div>
477
 
478
  <button id="scroll-top">▲</button>
479
 
480
- <!-- Lightbox -->
481
  <div id="lightbox">
482
  <div class="lb-header">
483
  <div class="lb-counter" id="lb-counter">1 / 1</div>
484
  <div class="lb-nav">
485
  <button class="lb-btn" id="lb-prev">◄</button>
486
  <button class="lb-btn" id="lb-next">►</button>
487
- <button class="lb-btn lb-close" id="lb-close">✕</button>
488
  </div>
489
  </div>
490
- <div class="lb-image-container">
491
- <div class="lb-spinner" id="lb-spinner"></div>
492
  <img id="lightbox-img" src="" alt="" referrerpolicy="no-referrer">
493
  </div>
494
- <div class="lb-footer">
495
- <div class="lb-filename" id="lb-filename"></div>
496
- </div>
497
  </div>
498
 
499
  <script>
500
- let allImages = [];
501
  let loadedCount = 0;
502
  let currentIdx = 0;
503
  let isLoading = false;
504
- const BATCH_SIZE = 100;
505
-
506
- const loadingScreen = document.getElementById('loading-screen');
507
- const galleryScreen = document.getElementById('gallery-screen');
508
- const grid = document.getElementById('gallery-grid');
509
- const loadMoreText = document.getElementById('load-more-text');
510
- const loadSpinner = document.getElementById('load-spinner');
511
- const scrollTopBtn = document.getElementById('scroll-top');
512
- const loadingText = document.getElementById('loading-text');
513
-
514
- const imageObserver = new IntersectionObserver((entries) => {
515
- entries.forEach(entry => {
516
- const img = entry.target;
517
- if (entry.isIntersecting) {
518
- if (img.dataset.src && !img.src) {
519
- img.src = img.dataset.src;
520
- img.onload = () => img.classList.add('loaded');
521
- img.onerror = () => {
522
- img.parentElement.querySelector('.placeholder').textContent = 'ERR';
523
- };
524
- }
525
- } else {
526
- const rect = img.getBoundingClientRect();
527
- const buffer = window.innerHeight * 3;
528
- if (rect.top > buffer || rect.bottom < -buffer) {
529
- if (img.src) {
530
- img.removeAttribute('src');
531
- img.classList.remove('loaded');
532
- }
533
- }
534
- }
535
- });
536
- }, { rootMargin: '500px' });
537
 
538
- const scrollObserver = new IntersectionObserver((entries) => {
539
- if (entries[0].isIntersecting && !isLoading) {
540
- loadMoreImages();
541
- }
542
- }, { rootMargin: '600px' });
 
 
 
543
 
544
- function loadMoreImages() {
545
  if (isLoading || loadedCount >= allImages.length) return;
546
-
547
  isLoading = true;
548
- loadSpinner.classList.add('active');
549
- loadMoreText.textContent = 'LOADING...';
550
 
551
- requestAnimationFrame(() => {
552
- const fragment = document.createDocumentFragment();
553
- const end = Math.min(loadedCount + BATCH_SIZE, allImages.length);
554
-
555
- for (let i = loadedCount; i < end; i++) {
556
- fragment.appendChild(createImageItem(allImages[i], i));
557
- }
558
-
559
- grid.appendChild(fragment);
560
-
561
- grid.querySelectorAll('img:not([data-observed])').forEach(img => {
562
- img.dataset.observed = 'true';
563
- imageObserver.observe(img);
564
- });
565
-
566
- loadedCount = end;
567
- isLoading = false;
568
- loadSpinner.classList.remove('active');
569
-
570
- if (loadedCount >= allImages.length) {
571
- loadMoreText.textContent = 'ALL ' + allImages.length + ' FILES LOADED';
572
- } else {
573
- loadMoreText.textContent = loadedCount + ' / ' + allImages.length;
574
- }
575
- });
576
- }
577
-
578
- function createImageItem(url, idx) {
579
- const item = document.createElement('div');
580
- item.className = 'gallery-item';
581
 
582
- const placeholder = document.createElement('div');
583
- placeholder.className = 'placeholder';
584
- placeholder.textContent = '#' + (idx + 1);
585
-
586
- const img = document.createElement('img');
587
- img.dataset.src = url;
588
- img.alt = '';
589
- img.referrerPolicy = 'no-referrer';
590
 
591
- const number = document.createElement('div');
592
- number.className = 'item-number';
593
- number.textContent = '#' + (idx + 1);
594
 
595
- item.appendChild(placeholder);
596
- item.appendChild(img);
597
- item.appendChild(number);
598
- item.onclick = () => openLightbox(idx);
599
 
600
- return item;
601
  }
602
 
603
- function showGallery(imageList) {
604
- allImages = imageList;
605
- loadedCount = 0;
606
-
607
- loadingScreen.style.display = 'none';
608
- galleryScreen.style.display = 'block';
609
-
610
- document.getElementById('gallery-count').innerHTML =
611
- allImages.length + ' <span>FILES</span>';
612
-
613
- grid.innerHTML = '';
614
- loadMoreImages();
615
-
616
- scrollObserver.observe(document.getElementById('load-more-trigger'));
617
- }
 
618
 
619
- function showError(msg) {
620
- loadingText.textContent = msg;
621
- loadingText.classList.add('error-message');
 
 
622
  }
623
 
624
- let scrollTicking = false;
 
625
  window.addEventListener('scroll', () => {
626
- if (!scrollTicking) {
627
- requestAnimationFrame(() => {
628
- scrollTopBtn.classList.toggle('visible', window.scrollY > 500);
629
- scrollTicking = false;
630
- });
631
- scrollTicking = true;
632
- }
633
- });
 
 
 
 
634
 
635
- scrollTopBtn.onclick = () => {
636
- window.scrollTo({ top: 0, behavior: 'smooth' });
637
- };
638
 
639
- const lightbox = document.getElementById('lightbox');
640
- const lbImg = document.getElementById('lightbox-img');
641
- const lbCounter = document.getElementById('lb-counter');
642
- const lbFilename = document.getElementById('lb-filename');
643
- const lbSpinner = document.getElementById('lb-spinner');
 
 
 
644
 
645
- function openLightbox(idx) {
646
- currentIdx = idx;
647
- updateLightbox();
648
  lightbox.classList.add('active');
649
  document.body.style.overflow = 'hidden';
 
650
  }
651
 
652
  function closeLightbox() {
653
  lightbox.classList.remove('active');
654
  document.body.style.overflow = '';
655
  lbImg.src = '';
656
- lbImg.classList.remove('loaded');
657
  }
658
 
659
- function updateLightbox() {
660
- lbSpinner.classList.remove('hidden');
661
- lbImg.classList.remove('loaded');
662
 
663
- lbImg.onload = () => {
664
- lbSpinner.classList.add('hidden');
665
- lbImg.classList.add('loaded');
666
- };
667
-
668
- lbImg.src = allImages[currentIdx];
669
- lbCounter.textContent = (currentIdx + 1) + ' / ' + allImages.length;
670
- lbFilename.textContent = 'FILE #' + (currentIdx + 1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
671
  }
672
 
673
- function navigate(dir) {
674
- currentIdx = (currentIdx + dir + allImages.length) % allImages.length;
675
- updateLightbox();
676
  }
677
 
678
- document.getElementById('lb-close').onclick = closeLightbox;
679
- document.getElementById('lb-prev').onclick = () => navigate(-1);
680
- document.getElementById('lb-next').onclick = () => navigate(1);
 
 
 
 
681
 
682
  document.addEventListener('keydown', e => {
683
- if (lightbox.classList.contains('active')) {
684
- if (e.key === 'Escape') closeLightbox();
685
- if (e.key === 'ArrowLeft') navigate(-1);
686
- if (e.key === 'ArrowRight') navigate(1);
687
- }
688
  });
689
 
690
- let touchStartX = 0;
691
- const lbImageContainer = document.querySelector('.lb-image-container');
692
-
693
- lbImageContainer.addEventListener('touchstart', e => {
694
- touchStartX = e.touches[0].clientX;
695
  }, { passive: true });
696
 
697
- lbImageContainer.addEventListener('touchend', e => {
698
- const diff = touchStartX - e.changedTouches[0].clientX;
699
- if (Math.abs(diff) > 50) {
700
- navigate(diff > 0 ? 1 : -1);
701
- }
702
  }, { passive: true });
703
 
 
704
  fetch('/status')
705
  .then(r => r.json())
706
  .then(data => {
707
- if (data.status === 'complete' && data.images.length > 0) {
708
- showGallery(data.images);
 
 
 
 
709
  } else {
710
- showError('NO IMAGES FOUND');
 
711
  }
712
  })
713
- .catch(() => showError('CONNECTION ERROR'));
 
 
 
714
  </script>
715
  </body>
716
  </html>'''
 
56
  z-index: 100;
57
  border-bottom: 3px solid #fff;
58
  text-transform: uppercase;
 
 
 
 
 
 
59
  }
60
 
61
  .classified-banner {
 
68
  letter-spacing: 4px;
69
  }
70
 
 
71
  #loading-screen {
72
  display: flex;
73
  flex-direction: column;
 
78
  }
79
 
80
  .spinner {
81
+ width: 50px;
82
+ height: 50px;
83
+ border: 3px solid #333;
84
  border-top-color: #fff;
85
  border-radius: 50%;
86
+ animation: spin 0.6s linear infinite;
87
+ margin-bottom: 20px;
88
  }
89
 
90
  @keyframes spin {
 
95
  font-size: 14px;
96
  color: #888;
97
  letter-spacing: 3px;
 
98
  }
99
 
100
  .error-message {
101
  color: #f44 !important;
102
  }
103
 
 
104
  #gallery-screen {
105
  display: none;
106
  padding: 20px;
 
107
  }
108
 
109
  .gallery-header {
110
  text-align: center;
111
+ margin-bottom: 20px;
112
+ padding-bottom: 15px;
113
  border-bottom: 1px solid #333;
114
  }
115
 
 
117
  font-size: 12px;
118
  color: #888;
119
  letter-spacing: 4px;
120
+ margin-bottom: 8px;
 
121
  }
122
 
123
  .gallery-count {
124
+ font-size: 28px;
125
  font-weight: 800;
126
  color: #fff;
127
+ letter-spacing: 3px;
128
  }
129
 
130
  .gallery-count span {
131
  color: #666;
132
+ font-size: 14px;
 
 
 
 
 
 
 
133
  }
134
 
135
  .gallery-grid {
136
  display: grid;
137
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
138
+ gap: 10px;
139
  }
140
 
141
  .gallery-item {
 
145
  cursor: pointer;
146
  background: #111;
147
  position: relative;
148
+ contain: layout style paint;
 
 
 
 
 
 
 
 
 
149
  }
150
 
151
  .gallery-item img {
152
  width: 100%;
153
  height: 100%;
154
  object-fit: cover;
155
+ display: block;
 
 
 
 
 
156
  }
157
 
158
  .gallery-item .placeholder {
 
161
  display: flex;
162
  align-items: center;
163
  justify-content: center;
164
+ color: #444;
165
  font-size: 10px;
 
166
  }
167
 
168
  .item-number {
 
170
  bottom: 0;
171
  left: 0;
172
  right: 0;
173
+ background: rgba(0,0,0,0.85);
174
  color: #fff;
175
+ padding: 4px;
176
  font-size: 9px;
177
  font-weight: 700;
 
178
  text-align: center;
179
  }
180
 
181
  #load-more-trigger {
182
+ height: 60px;
183
  display: flex;
184
  align-items: center;
185
  justify-content: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  }
187
 
188
  .load-more-text {
 
193
 
194
  #scroll-top {
195
  position: fixed;
196
+ bottom: 15px;
197
+ right: 15px;
198
  background: #000;
199
  border: 2px solid #fff;
200
  color: #fff;
201
+ width: 40px;
202
+ height: 40px;
203
+ font-size: 16px;
204
  cursor: pointer;
205
  z-index: 50;
206
  display: none;
 
 
 
 
 
 
 
207
  }
208
 
209
  #scroll-top.visible {
210
  display: flex;
211
+ align-items: center;
212
+ justify-content: center;
213
  }
214
 
 
215
  #lightbox {
216
  display: none;
217
  position: fixed;
 
229
  display: flex;
230
  justify-content: space-between;
231
  align-items: center;
232
+ padding: 10px 15px;
233
  background: #000;
234
  border-bottom: 2px solid #fff;
 
235
  }
236
 
237
  .lb-counter {
 
243
 
244
  .lb-nav {
245
  display: flex;
246
+ gap: 8px;
247
  }
248
 
249
  .lb-btn {
 
251
  border: 2px solid #fff;
252
  color: #fff;
253
  cursor: pointer;
254
+ font-size: 14px;
255
+ padding: 6px 12px;
256
  font-family: inherit;
257
  font-weight: 700;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  }
259
 
260
  .lb-image-container {
 
262
  display: flex;
263
  align-items: center;
264
  justify-content: center;
265
+ padding: 10px;
266
  overflow: hidden;
267
  position: relative;
268
  }
 
271
  max-width: 100%;
272
  max-height: 100%;
273
  object-fit: contain;
274
+ border: 2px solid #fff;
 
 
 
 
 
 
275
  }
276
 
277
+ .lb-loading {
278
  position: absolute;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  color: #666;
280
+ font-size: 12px;
281
  }
282
 
 
283
  @media (max-width: 600px) {
284
  #header {
285
+ font-size: 16px;
286
+ letter-spacing: 5px;
287
+ padding: 18px 12px;
288
  }
289
 
290
  .classified-banner {
291
+ font-size: 8px;
292
  letter-spacing: 2px;
293
+ padding: 5px;
294
  }
295
 
296
  .gallery-grid {
297
  grid-template-columns: repeat(3, 1fr);
298
+ gap: 6px;
299
  }
300
 
301
  .gallery-count {
302
+ font-size: 22px;
303
  }
304
 
305
  .item-number {
 
307
  padding: 3px;
308
  }
309
 
 
 
 
 
 
 
 
 
 
310
  .lb-btn {
311
+ font-size: 12px;
312
+ padding: 5px 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  }
314
  }
315
 
 
324
  <div id="header">EPSTEIN FILES</div>
325
  <div class="classified-banner">★ HERE ARE ALL THE IMAGES ★</div>
326
 
 
327
  <div id="loading-screen">
328
  <div class="spinner"></div>
329
  <div class="loading-text" id="loading-text">Loading...</div>
330
  </div>
331
 
 
332
  <div id="gallery-screen">
333
  <div class="gallery-header">
334
+ <div class="gallery-title">IMAGES</div>
335
  <div class="gallery-count" id="gallery-count">0 <span>FILES</span></div>
 
336
  </div>
337
  <div class="gallery-grid" id="gallery-grid"></div>
338
  <div id="load-more-trigger">
339
+ <div class="load-more-text" id="load-more-text"></div>
 
340
  </div>
341
  </div>
342
 
343
  <button id="scroll-top">▲</button>
344
 
 
345
  <div id="lightbox">
346
  <div class="lb-header">
347
  <div class="lb-counter" id="lb-counter">1 / 1</div>
348
  <div class="lb-nav">
349
  <button class="lb-btn" id="lb-prev">◄</button>
350
  <button class="lb-btn" id="lb-next">►</button>
351
+ <button class="lb-btn" id="lb-close">✕</button>
352
  </div>
353
  </div>
354
+ <div class="lb-image-container" id="lb-container">
355
+ <div class="lb-loading" id="lb-loading">Loading...</div>
356
  <img id="lightbox-img" src="" alt="" referrerpolicy="no-referrer">
357
  </div>
 
 
 
358
  </div>
359
 
360
  <script>
361
+ const allImages = [];
362
  let loadedCount = 0;
363
  let currentIdx = 0;
364
  let isLoading = false;
365
+ const BATCH = 50;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
 
367
+ const $ = id => document.getElementById(id);
368
+ const grid = $('gallery-grid');
369
+ const lightbox = $('lightbox');
370
+ const lbImg = $('lightbox-img');
371
+ const lbLoading = $('lb-loading');
372
+
373
+ // Simple image cache
374
+ const cache = new Map();
375
 
376
+ function loadBatch() {
377
  if (isLoading || loadedCount >= allImages.length) return;
 
378
  isLoading = true;
 
 
379
 
380
+ const end = Math.min(loadedCount + BATCH, allImages.length);
381
+ const frag = document.createDocumentFragment();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
 
383
+ for (let i = loadedCount; i < end; i++) {
384
+ const div = document.createElement('div');
385
+ div.className = 'gallery-item';
386
+ div.innerHTML = '<div class="placeholder">#' + (i+1) + '</div><div class="item-number">#' + (i+1) + '</div>';
387
+ div.dataset.idx = i;
388
+ frag.appendChild(div);
389
+ }
 
390
 
391
+ grid.appendChild(frag);
392
+ loadedCount = end;
393
+ isLoading = false;
394
 
395
+ $('load-more-text').textContent = loadedCount >= allImages.length ?
396
+ 'ALL ' + allImages.length + ' LOADED' : loadedCount + ' / ' + allImages.length;
 
 
397
 
398
+ observeVisible();
399
  }
400
 
401
+ // Only observe items in viewport
402
+ const observer = new IntersectionObserver(entries => {
403
+ entries.forEach(e => {
404
+ const div = e.target;
405
+ const idx = parseInt(div.dataset.idx);
406
+
407
+ if (e.isIntersecting) {
408
+ if (!div.querySelector('img')) {
409
+ const img = document.createElement('img');
410
+ img.referrerPolicy = 'no-referrer';
411
+ img.src = allImages[idx];
412
+ div.insertBefore(img, div.querySelector('.item-number'));
413
+ }
414
+ }
415
+ });
416
+ }, { rootMargin: '200px' });
417
 
418
+ function observeVisible() {
419
+ grid.querySelectorAll('.gallery-item:not([data-obs])').forEach(el => {
420
+ el.dataset.obs = '1';
421
+ observer.observe(el);
422
+ });
423
  }
424
 
425
+ // Scroll load
426
+ let scrollTimer;
427
  window.addEventListener('scroll', () => {
428
+ if (scrollTimer) return;
429
+ scrollTimer = setTimeout(() => {
430
+ scrollTimer = null;
431
+
432
+ $('scroll-top').classList.toggle('visible', window.scrollY > 400);
433
+
434
+ const trigger = $('load-more-trigger');
435
+ if (trigger.getBoundingClientRect().top < window.innerHeight + 300) {
436
+ loadBatch();
437
+ }
438
+ }, 100);
439
+ }, { passive: true });
440
 
441
+ $('scroll-top').onclick = () => window.scrollTo({ top: 0, behavior: 'smooth' });
 
 
442
 
443
+ // Gallery click
444
+ grid.addEventListener('click', e => {
445
+ const item = e.target.closest('.gallery-item');
446
+ if (item) {
447
+ currentIdx = parseInt(item.dataset.idx);
448
+ openLightbox();
449
+ }
450
+ });
451
 
452
+ function openLightbox() {
 
 
453
  lightbox.classList.add('active');
454
  document.body.style.overflow = 'hidden';
455
+ showImage();
456
  }
457
 
458
  function closeLightbox() {
459
  lightbox.classList.remove('active');
460
  document.body.style.overflow = '';
461
  lbImg.src = '';
 
462
  }
463
 
464
+ function showImage() {
465
+ const url = allImages[currentIdx];
466
+ $('lb-counter').textContent = (currentIdx + 1) + ' / ' + allImages.length;
467
 
468
+ if (cache.has(url)) {
469
+ lbImg.src = url;
470
+ lbLoading.style.display = 'none';
471
+ } else {
472
+ lbLoading.style.display = 'block';
473
+ lbImg.style.opacity = '0';
474
+
475
+ const img = new Image();
476
+ img.onload = () => {
477
+ cache.set(url, true);
478
+ lbImg.src = url;
479
+ lbImg.style.opacity = '1';
480
+ lbLoading.style.display = 'none';
481
+ preloadAdjacent();
482
+ };
483
+ img.src = url;
484
+ }
485
+ }
486
+
487
+ function preloadAdjacent() {
488
+ [-1, 1].forEach(d => {
489
+ const idx = (currentIdx + d + allImages.length) % allImages.length;
490
+ const url = allImages[idx];
491
+ if (!cache.has(url)) {
492
+ const img = new Image();
493
+ img.onload = () => cache.set(url, true);
494
+ img.src = url;
495
+ }
496
+ });
497
  }
498
 
499
+ function nav(d) {
500
+ currentIdx = (currentIdx + d + allImages.length) % allImages.length;
501
+ showImage();
502
  }
503
 
504
+ $('lb-close').onclick = closeLightbox;
505
+ $('lb-prev').onclick = () => nav(-1);
506
+ $('lb-next').onclick = () => nav(1);
507
+
508
+ $('lb-container').onclick = e => {
509
+ if (e.target === $('lb-container')) closeLightbox();
510
+ };
511
 
512
  document.addEventListener('keydown', e => {
513
+ if (!lightbox.classList.contains('active')) return;
514
+ if (e.key === 'Escape') closeLightbox();
515
+ if (e.key === 'ArrowLeft') nav(-1);
516
+ if (e.key === 'ArrowRight') nav(1);
 
517
  });
518
 
519
+ // Touch swipe
520
+ let touchX = 0;
521
+ $('lb-container').addEventListener('touchstart', e => {
522
+ touchX = e.touches[0].clientX;
 
523
  }, { passive: true });
524
 
525
+ $('lb-container').addEventListener('touchend', e => {
526
+ const diff = touchX - e.changedTouches[0].clientX;
527
+ if (Math.abs(diff) > 50) nav(diff > 0 ? 1 : -1);
 
 
528
  }, { passive: true });
529
 
530
+ // Init
531
  fetch('/status')
532
  .then(r => r.json())
533
  .then(data => {
534
+ if (data.status === 'complete' && data.images.length) {
535
+ allImages.push(...data.images);
536
+ $('gallery-count').innerHTML = allImages.length + ' <span>FILES</span>';
537
+ $('loading-screen').style.display = 'none';
538
+ $('gallery-screen').style.display = 'block';
539
+ loadBatch();
540
  } else {
541
+ $('loading-text').textContent = 'NO IMAGES FOUND';
542
+ $('loading-text').classList.add('error-message');
543
  }
544
  })
545
+ .catch(() => {
546
+ $('loading-text').textContent = 'CONNECTION ERROR';
547
+ $('loading-text').classList.add('error-message');
548
+ });
549
  </script>
550
  </body>
551
  </html>'''