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 `
`;
}
@@ -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 @@
+
+