jerrycans commited on
Commit
cf44a9b
·
verified ·
1 Parent(s): cc00440

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +195 -164
app.py CHANGED
@@ -46,10 +46,8 @@ HTML_PAGE = '''<!DOCTYPE html>
46
  font-size: 24px;
47
  font-weight: 800;
48
  letter-spacing: 10px;
49
- position: fixed;
50
  top: 0;
51
- left: 0;
52
- right: 0;
53
  z-index: 100;
54
  border-bottom: 3px solid #fff;
55
  }
@@ -62,22 +60,14 @@ HTML_PAGE = '''<!DOCTYPE html>
62
  font-size: 10px;
63
  font-weight: 700;
64
  letter-spacing: 3px;
65
- position: fixed;
66
- top: 63px;
67
- left: 0;
68
- right: 0;
69
- z-index: 100;
70
  }
71
 
72
  #loading {
73
- position: fixed;
74
- inset: 0;
75
  display: flex;
76
  flex-direction: column;
77
  align-items: center;
78
  justify-content: center;
79
- background: #0a0a0a;
80
- z-index: 200;
81
  }
82
 
83
  .spinner {
@@ -102,56 +92,89 @@ HTML_PAGE = '''<!DOCTYPE html>
102
 
103
  #gallery {
104
  display: none;
105
- padding-top: 110px;
106
  }
107
 
108
- .gallery-info {
109
  text-align: center;
110
- padding: 15px;
 
111
  border-bottom: 1px solid #333;
112
- margin-bottom: 10px;
113
  }
114
 
115
- .gallery-info h2 {
116
- font-size: 24px;
117
  letter-spacing: 3px;
118
  }
119
 
120
- .gallery-info span {
121
  color: #666;
122
  font-size: 12px;
123
  }
124
 
125
- #grid-container {
126
- position: relative;
127
- margin: 0 10px;
 
 
128
  }
129
 
130
- .grid-item {
131
- position: absolute;
 
 
 
 
 
 
132
  background: #111;
133
  border: 2px solid #333;
134
- overflow: hidden;
135
  cursor: pointer;
 
136
  }
137
 
138
- .grid-item img {
139
  width: 100%;
140
  height: 100%;
141
  object-fit: cover;
142
  display: block;
143
  }
144
 
145
- .grid-item .num {
146
  position: absolute;
147
  bottom: 0;
148
  left: 0;
149
  right: 0;
150
- background: rgba(0,0,0,0.8);
151
  color: #fff;
152
  font-size: 9px;
153
- padding: 3px;
154
  text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  }
156
 
157
  #scroll-top {
@@ -184,7 +207,7 @@ HTML_PAGE = '''<!DOCTYPE html>
184
 
185
  #lightbox.open { display: flex; }
186
 
187
- .lb-top {
188
  display: flex;
189
  justify-content: space-between;
190
  align-items: center;
@@ -192,13 +215,13 @@ HTML_PAGE = '''<!DOCTYPE html>
192
  border-bottom: 2px solid #fff;
193
  }
194
 
195
- .lb-count {
196
  font-size: 14px;
197
  font-weight: 700;
198
  letter-spacing: 2px;
199
  }
200
 
201
- .lb-btns { display: flex; gap: 8px; }
202
 
203
  .lb-btn {
204
  background: none;
@@ -210,7 +233,7 @@ HTML_PAGE = '''<!DOCTYPE html>
210
  font-family: inherit;
211
  }
212
 
213
- .lb-mid {
214
  flex: 1;
215
  display: flex;
216
  align-items: center;
@@ -234,11 +257,16 @@ HTML_PAGE = '''<!DOCTYPE html>
234
 
235
  @media (max-width: 600px) {
236
  #header { font-size: 16px; letter-spacing: 5px; padding: 15px; }
237
- .banner { top: 53px; font-size: 8px; padding: 4px; }
238
- #gallery { padding-top: 85px; }
239
- .gallery-info h2 { font-size: 18px; }
 
240
  .lb-btn { padding: 5px 10px; font-size: 12px; }
241
  }
 
 
 
 
242
  </style>
243
  </head>
244
  <body>
@@ -251,143 +279,134 @@ HTML_PAGE = '''<!DOCTYPE html>
251
  </div>
252
 
253
  <div id="gallery">
254
- <div class="gallery-info">
255
- <h2 id="count">0</h2>
256
  <span>IMAGES</span>
 
 
 
 
 
257
  </div>
258
- <div id="grid-container"></div>
259
  </div>
260
 
261
  <button id="scroll-top">▲</button>
262
 
263
  <div id="lightbox">
264
- <div class="lb-top">
265
- <div class="lb-count" id="lb-count">1 / 1</div>
266
- <div class="lb-btns">
267
  <button class="lb-btn" id="lb-prev">◄</button>
268
  <button class="lb-btn" id="lb-next">►</button>
269
  <button class="lb-btn" id="lb-close">✕</button>
270
  </div>
271
  </div>
272
- <div class="lb-mid" id="lb-mid">
273
  <div id="lb-loader">Loading...</div>
274
  <img id="lb-img" src="" alt="" referrerpolicy="no-referrer">
275
  </div>
276
  </div>
277
 
278
  <script>
279
- let images = [];
 
280
  let currentIdx = 0;
281
- let cols, itemSize, renderedItems = {};
 
 
282
 
283
- const container = document.getElementById('grid-container');
284
  const lightbox = document.getElementById('lightbox');
285
  const lbImg = document.getElementById('lb-img');
286
  const lbLoader = document.getElementById('lb-loader');
287
  const scrollBtn = document.getElementById('scroll-top');
 
288
 
289
- // Calculate grid dimensions
290
- function calcGrid() {
291
- const w = container.clientWidth;
292
- cols = Math.floor(w / 150) || 1;
293
- if (cols > 6) cols = 6;
294
- itemSize = Math.floor(w / cols);
295
- }
296
-
297
- // Get which items should be visible
298
- function getVisibleRange() {
299
- const scrollY = window.scrollY;
300
- const viewH = window.innerHeight;
301
- const gridTop = container.getBoundingClientRect().top + scrollY;
302
 
303
- const startY = Math.max(0, scrollY - gridTop - 500);
304
- const endY = scrollY - gridTop + viewH + 500;
305
 
306
- const startRow = Math.floor(startY / itemSize);
307
- const endRow = Math.ceil(endY / itemSize);
 
 
 
 
 
 
 
 
 
 
308
 
309
- const start = Math.max(0, startRow * cols);
310
- const end = Math.min(images.length, (endRow + 1) * cols);
 
311
 
312
- return { start, end };
313
  }
314
 
315
- // Render only visible items
316
- function render() {
317
- const { start, end } = getVisibleRange();
 
318
 
319
- // Remove items outside range
320
- for (const idx in renderedItems) {
321
- const i = parseInt(idx);
322
- if (i < start || i >= end) {
323
- renderedItems[idx].remove();
324
- delete renderedItems[idx];
325
- }
326
- }
327
 
328
- // Add items in range
329
- for (let i = start; i < end; i++) {
330
- if (!renderedItems[i]) {
331
- const row = Math.floor(i / cols);
332
- const col = i % cols;
333
-
334
- const div = document.createElement('div');
335
- div.className = 'grid-item';
336
- div.style.cssText = `left:${col * itemSize}px;top:${row * itemSize}px;width:${itemSize - 4}px;height:${itemSize - 4}px;`;
337
- div.dataset.idx = i;
338
-
339
  const img = document.createElement('img');
340
  img.referrerPolicy = 'no-referrer';
341
- img.loading = 'lazy';
342
- img.src = images[i];
343
-
344
- const num = document.createElement('div');
345
- num.className = 'num';
346
- num.textContent = '#' + (i + 1);
347
-
348
- div.appendChild(img);
349
- div.appendChild(num);
350
- container.appendChild(div);
351
-
352
- renderedItems[i] = div;
353
  }
354
- }
355
- }
356
-
357
- // Set container height
358
- function setHeight() {
359
- const rows = Math.ceil(images.length / cols);
360
- container.style.height = (rows * itemSize) + 'px';
361
  }
362
 
363
- // Scroll handler with RAF
364
  let ticking = false;
365
  function onScroll() {
366
- if (!ticking) {
367
- requestAnimationFrame(() => {
368
- render();
369
- scrollBtn.classList.toggle('show', window.scrollY > 400);
370
- ticking = false;
371
- });
372
- ticking = true;
373
- }
 
 
 
 
 
 
 
 
 
 
374
  }
375
 
376
- // Resize handler
377
- function onResize() {
378
- calcGrid();
379
- setHeight();
380
- // Clear and re-render
381
- for (const idx in renderedItems) {
382
- renderedItems[idx].remove();
383
- }
384
- renderedItems = {};
385
- render();
386
- }
387
 
388
- // Click handler
389
- container.addEventListener('click', e => {
390
- const item = e.target.closest('.grid-item');
391
  if (item) {
392
  currentIdx = parseInt(item.dataset.idx);
393
  openLightbox();
@@ -395,12 +414,12 @@ HTML_PAGE = '''<!DOCTYPE html>
395
  });
396
 
397
  // Lightbox
398
- const cache = new Set();
399
 
400
  function openLightbox() {
401
  lightbox.classList.add('open');
402
  document.body.style.overflow = 'hidden';
403
- showImg();
404
  }
405
 
406
  function closeLightbox() {
@@ -408,40 +427,56 @@ HTML_PAGE = '''<!DOCTYPE html>
408
  document.body.style.overflow = '';
409
  }
410
 
411
- function showImg() {
412
- const url = images[currentIdx];
413
- document.getElementById('lb-count').textContent = (currentIdx + 1) + ' / ' + images.length;
414
 
415
- lbLoader.style.display = cache.has(url) ? 'none' : 'block';
416
- lbImg.style.display = 'none';
417
-
418
- lbImg.onload = () => {
419
- cache.add(url);
420
  lbLoader.style.display = 'none';
421
- lbImg.style.display = 'block';
422
- preload();
423
- };
424
- lbImg.src = url;
 
 
 
 
 
 
 
 
 
 
 
425
  }
426
 
427
- function preload() {
428
- [-1, 1, -2, 2].forEach(d => {
 
429
  const i = currentIdx + d;
430
- if (i >= 0 && i < images.length && !cache.has(images[i])) {
431
- new Image().src = images[i];
 
 
 
 
 
432
  }
433
- });
434
  }
435
 
436
  function nav(d) {
437
- currentIdx = (currentIdx + d + images.length) % images.length;
438
- showImg();
439
  }
440
 
441
  document.getElementById('lb-close').onclick = closeLightbox;
442
  document.getElementById('lb-prev').onclick = () => nav(-1);
443
  document.getElementById('lb-next').onclick = () => nav(1);
444
- document.getElementById('lb-mid').onclick = e => { if (e.target.id === 'lb-mid') closeLightbox(); };
 
 
 
445
 
446
  document.addEventListener('keydown', e => {
447
  if (!lightbox.classList.contains('open')) return;
@@ -452,29 +487,25 @@ HTML_PAGE = '''<!DOCTYPE html>
452
 
453
  // Touch swipe
454
  let touchX = 0;
455
- document.getElementById('lb-mid').addEventListener('touchstart', e => touchX = e.touches[0].clientX, { passive: true });
456
- document.getElementById('lb-mid').addEventListener('touchend', e => {
 
 
 
457
  const diff = touchX - e.changedTouches[0].clientX;
458
  if (Math.abs(diff) > 50) nav(diff > 0 ? 1 : -1);
459
  }, { passive: true });
460
 
461
- scrollBtn.onclick = () => window.scrollTo({ top: 0, behavior: 'smooth' });
462
-
463
- window.addEventListener('scroll', onScroll, { passive: true });
464
- window.addEventListener('resize', onResize);
465
-
466
  // Init
467
  fetch('/status')
468
  .then(r => r.json())
469
  .then(data => {
470
  if (data.status === 'complete' && data.images.length) {
471
- images = data.images;
472
- document.getElementById('count').textContent = images.length;
473
  document.getElementById('loading').style.display = 'none';
474
  document.getElementById('gallery').style.display = 'block';
475
- calcGrid();
476
- setHeight();
477
- render();
478
  } else {
479
  document.getElementById('loading-text').textContent = 'NO IMAGES';
480
  document.getElementById('loading-text').classList.add('error');
 
46
  font-size: 24px;
47
  font-weight: 800;
48
  letter-spacing: 10px;
49
+ position: sticky;
50
  top: 0;
 
 
51
  z-index: 100;
52
  border-bottom: 3px solid #fff;
53
  }
 
60
  font-size: 10px;
61
  font-weight: 700;
62
  letter-spacing: 3px;
 
 
 
 
 
63
  }
64
 
65
  #loading {
 
 
66
  display: flex;
67
  flex-direction: column;
68
  align-items: center;
69
  justify-content: center;
70
+ min-height: calc(100vh - 120px);
 
71
  }
72
 
73
  .spinner {
 
92
 
93
  #gallery {
94
  display: none;
95
+ padding: 15px;
96
  }
97
 
98
+ .gallery-header {
99
  text-align: center;
100
+ margin-bottom: 15px;
101
+ padding-bottom: 15px;
102
  border-bottom: 1px solid #333;
 
103
  }
104
 
105
+ .gallery-header h2 {
106
+ font-size: 28px;
107
  letter-spacing: 3px;
108
  }
109
 
110
+ .gallery-header span {
111
  color: #666;
112
  font-size: 12px;
113
  }
114
 
115
+ .gallery-header p {
116
+ color: #555;
117
+ font-size: 10px;
118
+ margin-top: 8px;
119
+ letter-spacing: 1px;
120
  }
121
 
122
+ #grid {
123
+ display: grid;
124
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
125
+ gap: 10px;
126
+ }
127
+
128
+ .item {
129
+ aspect-ratio: 1;
130
  background: #111;
131
  border: 2px solid #333;
132
+ position: relative;
133
  cursor: pointer;
134
+ overflow: hidden;
135
  }
136
 
137
+ .item img {
138
  width: 100%;
139
  height: 100%;
140
  object-fit: cover;
141
  display: block;
142
  }
143
 
144
+ .item .num {
145
  position: absolute;
146
  bottom: 0;
147
  left: 0;
148
  right: 0;
149
+ background: rgba(0,0,0,0.85);
150
  color: #fff;
151
  font-size: 9px;
152
+ padding: 4px;
153
  text-align: center;
154
+ font-weight: 700;
155
+ }
156
+
157
+ .item .placeholder {
158
+ position: absolute;
159
+ inset: 0;
160
+ display: flex;
161
+ align-items: center;
162
+ justify-content: center;
163
+ color: #333;
164
+ font-size: 10px;
165
+ }
166
+
167
+ #load-trigger {
168
+ height: 60px;
169
+ display: flex;
170
+ align-items: center;
171
+ justify-content: center;
172
+ }
173
+
174
+ #load-text {
175
+ color: #555;
176
+ font-size: 10px;
177
+ letter-spacing: 2px;
178
  }
179
 
180
  #scroll-top {
 
207
 
208
  #lightbox.open { display: flex; }
209
 
210
+ .lb-header {
211
  display: flex;
212
  justify-content: space-between;
213
  align-items: center;
 
215
  border-bottom: 2px solid #fff;
216
  }
217
 
218
+ .lb-counter {
219
  font-size: 14px;
220
  font-weight: 700;
221
  letter-spacing: 2px;
222
  }
223
 
224
+ .lb-nav { display: flex; gap: 8px; }
225
 
226
  .lb-btn {
227
  background: none;
 
233
  font-family: inherit;
234
  }
235
 
236
+ .lb-body {
237
  flex: 1;
238
  display: flex;
239
  align-items: center;
 
257
 
258
  @media (max-width: 600px) {
259
  #header { font-size: 16px; letter-spacing: 5px; padding: 15px; }
260
+ .banner { font-size: 8px; padding: 4px; }
261
+ #grid { grid-template-columns: repeat(3, 1fr); gap: 6px; }
262
+ .gallery-header h2 { font-size: 22px; }
263
+ .item .num { font-size: 8px; padding: 3px; }
264
  .lb-btn { padding: 5px 10px; font-size: 12px; }
265
  }
266
+
267
+ @media (max-width: 380px) {
268
+ #grid { grid-template-columns: repeat(2, 1fr); }
269
+ }
270
  </style>
271
  </head>
272
  <body>
 
279
  </div>
280
 
281
  <div id="gallery">
282
+ <div class="gallery-header">
283
+ <h2 id="total-count">0</h2>
284
  <span>IMAGES</span>
285
+ <p>Click to enlarge</p>
286
+ </div>
287
+ <div id="grid"></div>
288
+ <div id="load-trigger">
289
+ <div id="load-text">SCROLL FOR MORE</div>
290
  </div>
 
291
  </div>
292
 
293
  <button id="scroll-top">▲</button>
294
 
295
  <div id="lightbox">
296
+ <div class="lb-header">
297
+ <div class="lb-counter" id="lb-counter">1 / 1</div>
298
+ <div class="lb-nav">
299
  <button class="lb-btn" id="lb-prev">◄</button>
300
  <button class="lb-btn" id="lb-next">►</button>
301
  <button class="lb-btn" id="lb-close">✕</button>
302
  </div>
303
  </div>
304
+ <div class="lb-body" id="lb-body">
305
  <div id="lb-loader">Loading...</div>
306
  <img id="lb-img" src="" alt="" referrerpolicy="no-referrer">
307
  </div>
308
  </div>
309
 
310
  <script>
311
+ let allImages = [];
312
+ let loadedCount = 0;
313
  let currentIdx = 0;
314
+ let isLoading = false;
315
+ const BATCH = 60;
316
+ const BUFFER = 800; // pixels above/below viewport to keep loaded
317
 
318
+ const grid = document.getElementById('grid');
319
  const lightbox = document.getElementById('lightbox');
320
  const lbImg = document.getElementById('lb-img');
321
  const lbLoader = document.getElementById('lb-loader');
322
  const scrollBtn = document.getElementById('scroll-top');
323
+ const loadText = document.getElementById('load-text');
324
 
325
+ // Load a batch of items
326
+ function loadBatch() {
327
+ if (isLoading || loadedCount >= allImages.length) return;
328
+ isLoading = true;
 
 
 
 
 
 
 
 
 
329
 
330
+ const end = Math.min(loadedCount + BATCH, allImages.length);
331
+ const frag = document.createDocumentFragment();
332
 
333
+ for (let i = loadedCount; i < end; i++) {
334
+ const div = document.createElement('div');
335
+ div.className = 'item';
336
+ div.dataset.idx = i;
337
+ div.dataset.src = allImages[i];
338
+ div.innerHTML = '<div class="placeholder">#' + (i + 1) + '</div><div class="num">#' + (i + 1) + '</div>';
339
+ frag.appendChild(div);
340
+ }
341
+
342
+ grid.appendChild(frag);
343
+ loadedCount = end;
344
+ isLoading = false;
345
 
346
+ loadText.textContent = loadedCount >= allImages.length ?
347
+ 'ALL ' + allImages.length + ' LOADED' :
348
+ loadedCount + ' / ' + allImages.length;
349
 
350
+ manageImages();
351
  }
352
 
353
+ // Load/unload images based on viewport
354
+ function manageImages() {
355
+ const viewTop = window.scrollY - BUFFER;
356
+ const viewBottom = window.scrollY + window.innerHeight + BUFFER;
357
 
358
+ const items = grid.querySelectorAll('.item');
 
 
 
 
 
 
 
359
 
360
+ items.forEach(item => {
361
+ const rect = item.getBoundingClientRect();
362
+ const itemTop = rect.top + window.scrollY;
363
+ const itemBottom = itemTop + rect.height;
364
+
365
+ const inView = itemBottom > viewTop && itemTop < viewBottom;
366
+ const hasImg = item.querySelector('img');
367
+
368
+ if (inView && !hasImg) {
369
+ // Load image
 
370
  const img = document.createElement('img');
371
  img.referrerPolicy = 'no-referrer';
372
+ img.src = item.dataset.src;
373
+ item.insertBefore(img, item.querySelector('.num'));
374
+ } else if (!inView && hasImg) {
375
+ // Unload image to free memory
376
+ hasImg.remove();
 
 
 
 
 
 
 
377
  }
378
+ });
 
 
 
 
 
 
379
  }
380
 
381
+ // Scroll handler
382
  let ticking = false;
383
  function onScroll() {
384
+ if (ticking) return;
385
+ ticking = true;
386
+
387
+ requestAnimationFrame(() => {
388
+ ticking = false;
389
+
390
+ // Show/hide scroll button
391
+ scrollBtn.classList.toggle('show', window.scrollY > 400);
392
+
393
+ // Load more if near bottom
394
+ const trigger = document.getElementById('load-trigger');
395
+ if (trigger.getBoundingClientRect().top < window.innerHeight + 500) {
396
+ loadBatch();
397
+ }
398
+
399
+ // Manage which images are loaded
400
+ manageImages();
401
+ });
402
  }
403
 
404
+ window.addEventListener('scroll', onScroll, { passive: true });
405
+ scrollBtn.onclick = () => window.scrollTo({ top: 0, behavior: 'smooth' });
 
 
 
 
 
 
 
 
 
406
 
407
+ // Grid click
408
+ grid.addEventListener('click', e => {
409
+ const item = e.target.closest('.item');
410
  if (item) {
411
  currentIdx = parseInt(item.dataset.idx);
412
  openLightbox();
 
414
  });
415
 
416
  // Lightbox
417
+ const imgCache = new Set();
418
 
419
  function openLightbox() {
420
  lightbox.classList.add('open');
421
  document.body.style.overflow = 'hidden';
422
+ showImage();
423
  }
424
 
425
  function closeLightbox() {
 
427
  document.body.style.overflow = '';
428
  }
429
 
430
+ function showImage() {
431
+ const url = allImages[currentIdx];
432
+ document.getElementById('lb-counter').textContent = (currentIdx + 1) + ' / ' + allImages.length;
433
 
434
+ if (imgCache.has(url)) {
 
 
 
 
435
  lbLoader.style.display = 'none';
436
+ lbImg.src = url;
437
+ } else {
438
+ lbLoader.style.display = 'block';
439
+ lbImg.style.opacity = '0';
440
+
441
+ const tmp = new Image();
442
+ tmp.onload = () => {
443
+ imgCache.add(url);
444
+ lbImg.src = url;
445
+ lbImg.style.opacity = '1';
446
+ lbLoader.style.display = 'none';
447
+ preloadNearby();
448
+ };
449
+ tmp.src = url;
450
+ }
451
  }
452
 
453
+ function preloadNearby() {
454
+ for (let d = -2; d <= 2; d++) {
455
+ if (d === 0) continue;
456
  const i = currentIdx + d;
457
+ if (i >= 0 && i < allImages.length) {
458
+ const url = allImages[i];
459
+ if (!imgCache.has(url)) {
460
+ const img = new Image();
461
+ img.onload = () => imgCache.add(url);
462
+ img.src = url;
463
+ }
464
  }
465
+ }
466
  }
467
 
468
  function nav(d) {
469
+ currentIdx = (currentIdx + d + allImages.length) % allImages.length;
470
+ showImage();
471
  }
472
 
473
  document.getElementById('lb-close').onclick = closeLightbox;
474
  document.getElementById('lb-prev').onclick = () => nav(-1);
475
  document.getElementById('lb-next').onclick = () => nav(1);
476
+
477
+ document.getElementById('lb-body').onclick = e => {
478
+ if (e.target.id === 'lb-body') closeLightbox();
479
+ };
480
 
481
  document.addEventListener('keydown', e => {
482
  if (!lightbox.classList.contains('open')) return;
 
487
 
488
  // Touch swipe
489
  let touchX = 0;
490
+ document.getElementById('lb-body').addEventListener('touchstart', e => {
491
+ touchX = e.touches[0].clientX;
492
+ }, { passive: true });
493
+
494
+ document.getElementById('lb-body').addEventListener('touchend', e => {
495
  const diff = touchX - e.changedTouches[0].clientX;
496
  if (Math.abs(diff) > 50) nav(diff > 0 ? 1 : -1);
497
  }, { passive: true });
498
 
 
 
 
 
 
499
  // Init
500
  fetch('/status')
501
  .then(r => r.json())
502
  .then(data => {
503
  if (data.status === 'complete' && data.images.length) {
504
+ allImages = data.images;
505
+ document.getElementById('total-count').textContent = allImages.length;
506
  document.getElementById('loading').style.display = 'none';
507
  document.getElementById('gallery').style.display = 'block';
508
+ loadBatch();
 
 
509
  } else {
510
  document.getElementById('loading-text').textContent = 'NO IMAGES';
511
  document.getElementById('loading-text').classList.add('error');