thefiles / app.py
jerrycans's picture
Update app.py
cf44a9b verified
from flask import Flask, jsonify
import os
app = Flask(__name__)
LINKS_FILE = "links.txt"
state = {"status": "idle", "images": []}
def load_links():
links = []
if os.path.exists(LINKS_FILE):
with open(LINKS_FILE, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
links.append(line)
return links
def init_images():
global state
links = load_links()
state = {"status": "complete" if links else "error", "images": links}
HTML_PAGE = '''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="referrer" content="no-referrer">
<title>EPSTEIN FILES</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0a0a0a;
font-family: 'Courier New', monospace;
color: #fff;
}
#header {
background: #000;
color: #fff;
text-align: center;
padding: 20px;
font-size: 24px;
font-weight: 800;
letter-spacing: 10px;
position: sticky;
top: 0;
z-index: 100;
border-bottom: 3px solid #fff;
}
.banner {
background: #fff;
color: #000;
text-align: center;
padding: 6px;
font-size: 10px;
font-weight: 700;
letter-spacing: 3px;
}
#loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: calc(100vh - 120px);
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid #333;
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.6s linear infinite;
margin-bottom: 15px;
}
@keyframes spin { to { transform: rotate(360deg); } }
#loading-text {
color: #888;
font-size: 12px;
letter-spacing: 2px;
}
.error { color: #f44 !important; }
#gallery {
display: none;
padding: 15px;
}
.gallery-header {
text-align: center;
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid #333;
}
.gallery-header h2 {
font-size: 28px;
letter-spacing: 3px;
}
.gallery-header span {
color: #666;
font-size: 12px;
}
.gallery-header p {
color: #555;
font-size: 10px;
margin-top: 8px;
letter-spacing: 1px;
}
#grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 10px;
}
.item {
aspect-ratio: 1;
background: #111;
border: 2px solid #333;
position: relative;
cursor: pointer;
overflow: hidden;
}
.item img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.item .num {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0,0,0,0.85);
color: #fff;
font-size: 9px;
padding: 4px;
text-align: center;
font-weight: 700;
}
.item .placeholder {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: #333;
font-size: 10px;
}
#load-trigger {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
#load-text {
color: #555;
font-size: 10px;
letter-spacing: 2px;
}
#scroll-top {
position: fixed;
bottom: 15px;
right: 15px;
background: #000;
border: 2px solid #fff;
color: #fff;
width: 40px;
height: 40px;
font-size: 16px;
cursor: pointer;
z-index: 50;
display: none;
align-items: center;
justify-content: center;
}
#scroll-top.show { display: flex; }
#lightbox {
position: fixed;
inset: 0;
background: #000;
z-index: 300;
display: none;
flex-direction: column;
}
#lightbox.open { display: flex; }
.lb-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
border-bottom: 2px solid #fff;
}
.lb-counter {
font-size: 14px;
font-weight: 700;
letter-spacing: 2px;
}
.lb-nav { display: flex; gap: 8px; }
.lb-btn {
background: none;
border: 2px solid #fff;
color: #fff;
padding: 6px 12px;
font-size: 14px;
cursor: pointer;
font-family: inherit;
}
.lb-body {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
position: relative;
}
#lb-img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border: 2px solid #fff;
}
#lb-loader {
position: absolute;
color: #666;
font-size: 12px;
}
@media (max-width: 600px) {
#header { font-size: 16px; letter-spacing: 5px; padding: 15px; }
.banner { font-size: 8px; padding: 4px; }
#grid { grid-template-columns: repeat(3, 1fr); gap: 6px; }
.gallery-header h2 { font-size: 22px; }
.item .num { font-size: 8px; padding: 3px; }
.lb-btn { padding: 5px 10px; font-size: 12px; }
}
@media (max-width: 380px) {
#grid { grid-template-columns: repeat(2, 1fr); }
}
</style>
</head>
<body>
<div id="header">EPSTEIN FILES</div>
<div class="banner">★ ALL IMAGES ★</div>
<div id="loading">
<div class="spinner"></div>
<div id="loading-text">Loading...</div>
</div>
<div id="gallery">
<div class="gallery-header">
<h2 id="total-count">0</h2>
<span>IMAGES</span>
<p>Click to enlarge</p>
</div>
<div id="grid"></div>
<div id="load-trigger">
<div id="load-text">SCROLL FOR MORE</div>
</div>
</div>
<button id="scroll-top">▲</button>
<div id="lightbox">
<div class="lb-header">
<div class="lb-counter" id="lb-counter">1 / 1</div>
<div class="lb-nav">
<button class="lb-btn" id="lb-prev">◄</button>
<button class="lb-btn" id="lb-next">►</button>
<button class="lb-btn" id="lb-close">✕</button>
</div>
</div>
<div class="lb-body" id="lb-body">
<div id="lb-loader">Loading...</div>
<img id="lb-img" src="" alt="" referrerpolicy="no-referrer">
</div>
</div>
<script>
let allImages = [];
let loadedCount = 0;
let currentIdx = 0;
let isLoading = false;
const BATCH = 60;
const BUFFER = 800; // pixels above/below viewport to keep loaded
const grid = document.getElementById('grid');
const lightbox = document.getElementById('lightbox');
const lbImg = document.getElementById('lb-img');
const lbLoader = document.getElementById('lb-loader');
const scrollBtn = document.getElementById('scroll-top');
const loadText = document.getElementById('load-text');
// Load a batch of items
function loadBatch() {
if (isLoading || loadedCount >= allImages.length) return;
isLoading = true;
const end = Math.min(loadedCount + BATCH, allImages.length);
const frag = document.createDocumentFragment();
for (let i = loadedCount; i < end; i++) {
const div = document.createElement('div');
div.className = 'item';
div.dataset.idx = i;
div.dataset.src = allImages[i];
div.innerHTML = '<div class="placeholder">#' + (i + 1) + '</div><div class="num">#' + (i + 1) + '</div>';
frag.appendChild(div);
}
grid.appendChild(frag);
loadedCount = end;
isLoading = false;
loadText.textContent = loadedCount >= allImages.length ?
'ALL ' + allImages.length + ' LOADED' :
loadedCount + ' / ' + allImages.length;
manageImages();
}
// Load/unload images based on viewport
function manageImages() {
const viewTop = window.scrollY - BUFFER;
const viewBottom = window.scrollY + window.innerHeight + BUFFER;
const items = grid.querySelectorAll('.item');
items.forEach(item => {
const rect = item.getBoundingClientRect();
const itemTop = rect.top + window.scrollY;
const itemBottom = itemTop + rect.height;
const inView = itemBottom > viewTop && itemTop < viewBottom;
const hasImg = item.querySelector('img');
if (inView && !hasImg) {
// Load image
const img = document.createElement('img');
img.referrerPolicy = 'no-referrer';
img.src = item.dataset.src;
item.insertBefore(img, item.querySelector('.num'));
} else if (!inView && hasImg) {
// Unload image to free memory
hasImg.remove();
}
});
}
// Scroll handler
let ticking = false;
function onScroll() {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
ticking = false;
// Show/hide scroll button
scrollBtn.classList.toggle('show', window.scrollY > 400);
// Load more if near bottom
const trigger = document.getElementById('load-trigger');
if (trigger.getBoundingClientRect().top < window.innerHeight + 500) {
loadBatch();
}
// Manage which images are loaded
manageImages();
});
}
window.addEventListener('scroll', onScroll, { passive: true });
scrollBtn.onclick = () => window.scrollTo({ top: 0, behavior: 'smooth' });
// Grid click
grid.addEventListener('click', e => {
const item = e.target.closest('.item');
if (item) {
currentIdx = parseInt(item.dataset.idx);
openLightbox();
}
});
// Lightbox
const imgCache = new Set();
function openLightbox() {
lightbox.classList.add('open');
document.body.style.overflow = 'hidden';
showImage();
}
function closeLightbox() {
lightbox.classList.remove('open');
document.body.style.overflow = '';
}
function showImage() {
const url = allImages[currentIdx];
document.getElementById('lb-counter').textContent = (currentIdx + 1) + ' / ' + allImages.length;
if (imgCache.has(url)) {
lbLoader.style.display = 'none';
lbImg.src = url;
} else {
lbLoader.style.display = 'block';
lbImg.style.opacity = '0';
const tmp = new Image();
tmp.onload = () => {
imgCache.add(url);
lbImg.src = url;
lbImg.style.opacity = '1';
lbLoader.style.display = 'none';
preloadNearby();
};
tmp.src = url;
}
}
function preloadNearby() {
for (let d = -2; d <= 2; d++) {
if (d === 0) continue;
const i = currentIdx + d;
if (i >= 0 && i < allImages.length) {
const url = allImages[i];
if (!imgCache.has(url)) {
const img = new Image();
img.onload = () => imgCache.add(url);
img.src = url;
}
}
}
}
function nav(d) {
currentIdx = (currentIdx + d + allImages.length) % allImages.length;
showImage();
}
document.getElementById('lb-close').onclick = closeLightbox;
document.getElementById('lb-prev').onclick = () => nav(-1);
document.getElementById('lb-next').onclick = () => nav(1);
document.getElementById('lb-body').onclick = e => {
if (e.target.id === 'lb-body') closeLightbox();
};
document.addEventListener('keydown', e => {
if (!lightbox.classList.contains('open')) return;
if (e.key === 'Escape') closeLightbox();
if (e.key === 'ArrowLeft') nav(-1);
if (e.key === 'ArrowRight') nav(1);
});
// Touch swipe
let touchX = 0;
document.getElementById('lb-body').addEventListener('touchstart', e => {
touchX = e.touches[0].clientX;
}, { passive: true });
document.getElementById('lb-body').addEventListener('touchend', e => {
const diff = touchX - e.changedTouches[0].clientX;
if (Math.abs(diff) > 50) nav(diff > 0 ? 1 : -1);
}, { passive: true });
// Init
fetch('/status')
.then(r => r.json())
.then(data => {
if (data.status === 'complete' && data.images.length) {
allImages = data.images;
document.getElementById('total-count').textContent = allImages.length;
document.getElementById('loading').style.display = 'none';
document.getElementById('gallery').style.display = 'block';
loadBatch();
} else {
document.getElementById('loading-text').textContent = 'NO IMAGES';
document.getElementById('loading-text').classList.add('error');
}
})
.catch(() => {
document.getElementById('loading-text').textContent = 'ERROR';
document.getElementById('loading-text').classList.add('error');
});
</script>
</body>
</html>'''
@app.route('/')
def index():
return HTML_PAGE, 200, {'Referrer-Policy': 'no-referrer'}
@app.route('/status')
def get_status():
return jsonify(state), 200, {'Referrer-Policy': 'no-referrer'}
init_images()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=7860, threaded=True)