diff --git a/app.py b/app.py index 0e49129..b5455fc 100644 --- a/app.py +++ b/app.py @@ -235,6 +235,20 @@ def get_calendar(): return jsonify(empty_week_payload(week_start_date, members_for_filter)) +@app.route('/api/photos') +def get_photos(): + """Return a shuffled list of all image URLs from the photos directory.""" + photos_dir = app.config['PHOTOS_DIR'] + if not os.path.exists(photos_dir): + return jsonify([]) + images = [ + f'/static/photos/{f}' for f in os.listdir(photos_dir) + if f.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp')) + ] + random.shuffle(images) + return jsonify(images) + + @app.route('/api/background') def get_background(): """Get a random background image.""" @@ -289,8 +303,8 @@ def get_joke(): if __name__ == '__main__': - # Create backgrounds directory if it doesn't exist os.makedirs(app.config['BACKGROUNDS_DIR'], exist_ok=True) + os.makedirs(app.config['PHOTOS_DIR'], exist_ok=True) os.makedirs(app.config['CREDENTIALS_DIR'], exist_ok=True) # Run the app diff --git a/config.py b/config.py index f18cdf5..630098f 100644 --- a/config.py +++ b/config.py @@ -28,6 +28,7 @@ class Config: # Directories BACKGROUNDS_DIR = os.path.join('static', 'backgrounds') + PHOTOS_DIR = os.path.join('static', 'photos') CREDENTIALS_DIR = 'credentials' # Family members for event color-coding. diff --git a/static/css/style.css b/static/css/style.css index d0ef035..c2f0c65 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -299,6 +299,45 @@ html, body { transform: translateY(0); } +/* ---------- Photo slideshow screensaver ---------- */ + +.slideshow-overlay { + position: fixed; + inset: 0; + background: #000; + z-index: 100; +} + +.slideshow-overlay.hidden { + display: none; +} + +.slide-img { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + opacity: 0; + transition: opacity 1.5s ease-in-out; +} + +.slide-img.active { + opacity: 1; +} + +.slideshow-hint { + position: absolute; + bottom: 32px; + left: 50%; + transform: translateX(-50%); + color: rgba(255, 255, 255, 0.35); + font-size: 13px; + letter-spacing: 0.04em; + pointer-events: none; + white-space: nowrap; +} + /* Scrollbar styling for day columns */ .day-events::-webkit-scrollbar { width: 4px; diff --git a/static/js/app.js b/static/js/app.js index 88d9e9a..1e6f9e0 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -4,6 +4,9 @@ const INTERVALS = { CALENDAR: 300000 }; +const INACTIVITY_TIMEOUT = 60 * 60 * 1000; // 1 hour +const SLIDE_DURATION = 15 * 1000; // 15 seconds per photo + const DAY_LABELS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; const MONTH_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; @@ -12,6 +15,14 @@ let filterOptionsBuilt = false; let lastData = null; let lastWeatherData = null; +// Slideshow state +let photos = []; +let slideIndex = 0; +let activeSlot = 'a'; +let slideTimer = null; +let inactivityTimer = null; +let slideshowActive = false; + function weatherIcon(code) { return `${code}`; } @@ -37,6 +48,7 @@ document.addEventListener('DOMContentLoaded', () => { updateTime(); updateWeather(); fetchCalendar(); + initSlideshow(); document.getElementById('member-filter').addEventListener('change', (e) => { activeFilter = e.target.value; @@ -208,3 +220,96 @@ function showAddToast() { clearTimeout(showAddToast._timer); showAddToast._timer = setTimeout(() => toast.classList.remove('visible'), 3000); } + +// ---------- Slideshow screensaver ---------- + +async function initSlideshow() { + try { + const res = await fetch('/api/photos'); + photos = await res.json(); + } catch (e) { + console.error('Could not load photos:', e); + } + + // Any interaction resets the inactivity timer (or dismisses the slideshow) + ['touchstart', 'click', 'mousemove', 'keydown'].forEach(evt => { + document.addEventListener(evt, handleUserActivity, { passive: true }); + }); + + resetInactivityTimer(); +} + +function handleUserActivity() { + if (slideshowActive) { + hideSlideshow(); + } else { + resetInactivityTimer(); + } +} + +function resetInactivityTimer() { + clearTimeout(inactivityTimer); + if (photos.length > 0) { + inactivityTimer = setTimeout(showSlideshow, INACTIVITY_TIMEOUT); + } +} + +function showSlideshow() { + slideshowActive = true; + const overlay = document.getElementById('slideshow-overlay'); + overlay.classList.remove('hidden'); + + // Start from a random position in the shuffled list + slideIndex = Math.floor(Math.random() * photos.length); + activeSlot = 'a'; + + // Show first photo immediately; preload second into the inactive slot + loadSlide('a', photos[slideIndex]); + if (photos.length > 1) { + loadSlide('b', photos[(slideIndex + 1) % photos.length]); + } + + slideTimer = setInterval(advanceSlide, SLIDE_DURATION); +} + +function hideSlideshow() { + slideshowActive = false; + clearInterval(slideTimer); + slideTimer = null; + + const overlay = document.getElementById('slideshow-overlay'); + overlay.classList.add('hidden'); + + // Clear images to free memory + document.getElementById('slide-a').classList.remove('active'); + document.getElementById('slide-b').classList.remove('active'); + document.getElementById('slide-a').src = ''; + document.getElementById('slide-b').src = ''; + + resetInactivityTimer(); +} + +function loadSlide(slot, url) { + const img = document.getElementById(`slide-${slot}`); + img.src = url; + if (slot === activeSlot) { + // Slight delay lets the browser decode before fading in + requestAnimationFrame(() => requestAnimationFrame(() => img.classList.add('active'))); + } +} + +function advanceSlide() { + const nextSlot = activeSlot === 'a' ? 'b' : 'a'; + const current = document.getElementById(`slide-${activeSlot}`); + const next = document.getElementById(`slide-${nextSlot}`); + + // Fade in the preloaded next image, fade out current + next.classList.add('active'); + current.classList.remove('active'); + activeSlot = nextSlot; + + // Preload the one after next into the now-hidden slot + slideIndex = (slideIndex + 1) % photos.length; + const upcoming = photos[(slideIndex + 1) % photos.length]; + current.src = upcoming; +} diff --git a/templates/index.html b/templates/index.html index b90fc8b..0c9813a 100644 --- a/templates/index.html +++ b/templates/index.html @@ -25,6 +25,13 @@ + + +