2 Commits
Author SHA1 Message Date
luddieandClaude Sonnet 4.6 d9f3a98286 Simplify slideshow: load-on-demand, no preloading; add JS cache-busting
advanceSlide now sets src and waits for onload on the next slot directly —
no preloading, no transitionend, no setTimeout. Local files load in
milliseconds so preloading is unnecessary complexity that was causing
the slideshow to stall.

Also adds ?v=<mtime> cache-busting to the app.js script tag so Chromium
always picks up new JS after a deploy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-03 13:03:50 +12:00
luddieandClaude Sonnet 4.6 47d41b4a61 Fix slideshow stalling — replace transitionend with setTimeout for preload
transitionend is unreliable: if the Pi's display sleeps or a transition
is interrupted, the event never fires and the next slot's src is never
set. advanceSlide then waits forever on an onload that can't fire.
Replace with a 1600ms setTimeout (just past the 1.5s CSS transition)
which always fires. Also add a guard to kick-start the src if it's
blank when advanceSlide runs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-26 13:09:00 +12:00
3 changed files with 21 additions and 38 deletions
+3 -2
View File
@@ -20,8 +20,9 @@ joke_cache = {'data': None, 'timestamp': None}
@app.route('/') @app.route('/')
def index(): def index():
"""Render the main display page.""" js_path = os.path.join(app.static_folder, 'js', 'app.js')
return render_template('index.html') cache_bust = int(os.path.getmtime(js_path))
return render_template('index.html', cache_bust=cache_bust)
@app.route('/api/weather') @app.route('/api/weather')
+14 -32
View File
@@ -276,10 +276,6 @@ async function showSlideshow() {
activeSlot = 'a'; activeSlot = 'a';
loadSlide('a', photos[slideIndex]); loadSlide('a', photos[slideIndex]);
if (photos.length > 1) {
loadSlide('b', photos[(slideIndex + 1) % photos.length]);
}
slideTimer = setInterval(advanceSlide, SLIDE_DURATION); slideTimer = setInterval(advanceSlide, SLIDE_DURATION);
} }
@@ -291,29 +287,21 @@ function hideSlideshow() {
const overlay = document.getElementById('slideshow-overlay'); const overlay = document.getElementById('slideshow-overlay');
overlay.classList.add('hidden'); overlay.classList.add('hidden');
// Clear images to free memory const a = document.getElementById('slide-a');
document.getElementById('slide-a').classList.remove('active'); const b = document.getElementById('slide-b');
document.getElementById('slide-b').classList.remove('active'); a.onload = null; b.onload = null;
document.getElementById('slide-a').src = ''; a.classList.remove('active'); b.classList.remove('active');
document.getElementById('slide-b').src = ''; a.removeAttribute('src'); b.removeAttribute('src');
resetInactivityTimer(); resetInactivityTimer();
} }
function loadSlide(slot, url) { function loadSlide(slot, url) {
const img = document.getElementById(`slide-${slot}`); const img = document.getElementById(`slide-${slot}`);
if (slot === activeSlot) { const show = () => { img.onload = null; img.classList.add('active'); };
// Only fade in once the image is fully decoded — no blank-then-flash img.onload = show;
img.onload = () => { img.onload = null; img.classList.add('active'); };
img.src = url; img.src = url;
// Handle cached images that won't fire onload if (img.complete && img.naturalWidth > 0) show();
if (img.complete && img.naturalWidth > 0) {
img.onload = null;
img.classList.add('active');
}
} else {
img.src = url; // preload only
}
} }
function advanceSlide() { function advanceSlide() {
@@ -321,22 +309,16 @@ function advanceSlide() {
const current = document.getElementById(`slide-${activeSlot}`); const current = document.getElementById(`slide-${activeSlot}`);
const next = document.getElementById(`slide-${nextSlot}`); const next = document.getElementById(`slide-${nextSlot}`);
const doSwap = () => { slideIndex = (slideIndex + 1) % photos.length;
const swap = () => {
next.onload = null; next.onload = null;
next.classList.add('active'); next.classList.add('active');
current.classList.remove('active'); current.classList.remove('active');
activeSlot = nextSlot; activeSlot = nextSlot;
slideIndex = (slideIndex + 1) % photos.length;
const preloadSrc = photos[(slideIndex + 1) % photos.length];
// Only change src after the fade-out completes — changing it mid-transition
// causes the new image to appear at partial opacity (the flash the user sees)
current.addEventListener('transitionend', () => { current.src = preloadSrc; }, { once: true });
}; };
// If the preloaded image is ready, swap immediately; otherwise wait for it next.onload = swap;
if (next.complete && next.naturalWidth > 0) { next.src = photos[slideIndex];
doSwap(); if (next.complete && next.naturalWidth > 0) swap();
} else {
next.onload = doSwap;
}
} }
+1 -1
View File
@@ -32,6 +32,6 @@
<div class="slideshow-hint">Touch to return to calendar</div> <div class="slideshow-hint">Touch to return to calendar</div>
</div> </div>
<script src="{{ url_for('static', filename='js/app.js') }}"></script> <script src="{{ url_for('static', filename='js/app.js') }}?v={{ cache_bust }}"></script>
</body> </body>
</html> </html>