// === ImmichFrame - Frontend Application === (function () { 'use strict'; // --- State --- var config = {}; var assets = []; var currentIndex = -1; var activeLayer = 'a'; var slideshowTimer = null; var progressTimer = null; var progressStart = 0; var overlayVisible = true; var overlayTimeout = null; var selectedSource = null; var selectedAlbumId = null; var isRunning = false; var preloadedImages = {}; // --- DOM Elements --- var $setupScreen = document.getElementById('setup-screen'); var $slideshowScreen = document.getElementById('slideshow-screen'); var $connectionStatus = document.getElementById('connection-status'); var $setupContent = document.getElementById('setup-content'); var $setupError = document.getElementById('setup-error'); var $errorDetail = document.getElementById('error-detail'); var $albumsList = document.getElementById('albums-list'); var $btnStart = document.getElementById('btn-start'); var $layerA = document.getElementById('photo-layer-a'); var $layerB = document.getElementById('photo-layer-b'); var $bgBlur = document.getElementById('bg-blur'); var $clock = document.getElementById('clock'); var $dateDisplay = document.getElementById('date-display'); var $exifInfo = document.getElementById('exif-info'); var $progressFill = document.getElementById('progress-fill'); var $overlay = document.getElementById('overlay'); var $btnSettings = document.getElementById('btn-settings'); var $progressBar = document.getElementById('progress-bar'); // --- Initialization --- async function init() { document.body.classList.add('setup-mode'); try { // Load config var configRes = await fetch('/api/config'); config = await configRes.json(); if (!config.connected) { showError('API key not configured. Set IMMICH_API_KEY in your environment.'); return; } // Test connection var serverRes = await fetch('/api/server-info'); var serverInfo = await serverRes.json(); if (!serverInfo.ok) { showError('Cannot reach Immich server: ' + serverInfo.error); return; } $connectionStatus.textContent = 'Connected to Immich v' + serverInfo.version.major + '.' + serverInfo.version.minor + '.' + serverInfo.version.patch; $connectionStatus.classList.add('connected'); // Load albums await loadAlbums(); // If a default album is set, auto-start if (config.albumId) { selectedSource = 'album'; selectedAlbumId = config.albumId; $btnStart.disabled = false; startSlideshow(); return; } if (config.showFavoritesOnly) { selectedSource = 'favorites'; $btnStart.disabled = false; startSlideshow(); return; } } catch (err) { showError('Failed to initialize: ' + err.message); } } function showError(msg) { $setupContent.style.display = 'none'; $setupError.style.display = 'block'; $errorDetail.textContent = msg; } // --- Albums --- async function loadAlbums() { try { var res = await fetch('/api/albums'); var albums = await res.json(); if (!albums.length) { $albumsList.innerHTML = '
No albums found
'; return; } var html = ''; for (var i = 0; i < albums.length; i++) { var a = albums[i]; var thumbUrl = a.albumThumbnailAssetId ? '/api/assets/' + a.albumThumbnailAssetId + '/thumbnail?size=thumbnail' : ''; html += 'Failed to load albums
'; } } // --- Source Selection --- window.selectSource = function (source) { selectedSource = source; selectedAlbumId = null; // Update button states document.getElementById('btn-all-photos').classList.toggle('selected', source === 'random'); document.getElementById('btn-favorites').classList.toggle('selected', source === 'favorites'); // Deselect albums var albumItems = document.querySelectorAll('.album-item'); for (var i = 0; i < albumItems.length; i++) { albumItems[i].classList.remove('selected'); } $btnStart.disabled = false; }; window.selectAlbum = function (albumId, el) { selectedSource = 'album'; selectedAlbumId = albumId; // Update button states document.getElementById('btn-all-photos').classList.remove('selected'); document.getElementById('btn-favorites').classList.remove('selected'); var albumItems = document.querySelectorAll('.album-item'); for (var i = 0; i < albumItems.length; i++) { albumItems[i].classList.remove('selected'); } el.classList.add('selected'); $btnStart.disabled = false; }; // --- Load Assets --- async function loadAssets() { var res; if (selectedSource === 'album' && selectedAlbumId) { res = await fetch('/api/albums/' + selectedAlbumId); var album = await res.json(); assets = album.assets || []; } else if (selectedSource === 'favorites') { res = await fetch('/api/assets/favorites'); assets = await res.json(); } else { res = await fetch('/api/assets/random?count=100'); assets = await res.json(); } if (config.shuffle) { shuffleArray(assets); } } // --- Slideshow Control --- window.startSlideshow = async function () { if (!selectedSource) return; $btnStart.disabled = true; $btnStart.innerHTML = ' Loading…'; try { await loadAssets(); if (!assets.length) { $btnStart.textContent = 'No photos found'; setTimeout(function () { $btnStart.textContent = '▶ Start Slideshow'; $btnStart.disabled = false; }, 2000); return; } // Switch to slideshow $setupScreen.style.display = 'none'; $slideshowScreen.style.display = 'block'; document.body.classList.remove('setup-mode'); isRunning = true; // Apply config var transMs = (config.transitionDuration || 2) * 1000; $layerA.style.transition = 'opacity ' + transMs + 'ms ease'; $layerB.style.transition = 'opacity ' + transMs + 'ms ease'; $bgBlur.style.transition = 'opacity ' + (transMs * 0.75) + 'ms ease'; if (!config.showClock) $clock.style.display = 'none'; if (!config.showDate) $dateDisplay.style.display = 'none'; if (!config.showExif) $exifInfo.style.display = 'none'; if (!config.showProgress) $progressBar.style.display = 'none'; if (!config.backgroundBlur) $bgBlur.style.display = 'none'; // Start clock updateClock(); setInterval(updateClock, 1000); // Show first photo currentIndex = -1; showNextPhoto(); // Auto-hide overlay scheduleOverlayHide(); } catch (err) { $btnStart.textContent = 'Error: ' + err.message; setTimeout(function () { $btnStart.textContent = '▶ Start Slideshow'; $btnStart.disabled = false; }, 3000); } }; window.exitSlideshow = function () { isRunning = false; clearTimeout(slideshowTimer); clearInterval(progressTimer); $slideshowScreen.style.display = 'none'; $setupScreen.style.display = 'flex'; document.body.classList.add('setup-mode'); $btnStart.textContent = '▶ Start Slideshow'; $btnStart.disabled = false; // Reset layers $layerA.style.backgroundImage = ''; $layerB.style.backgroundImage = ''; $bgBlur.style.backgroundImage = ''; $layerA.classList.add('active'); $layerB.classList.remove('active'); $bgBlur.classList.remove('visible'); activeLayer = 'a'; }; // --- Photo Display --- function showNextPhoto() { currentIndex++; if (currentIndex >= assets.length) { // Reload and reshuffle for infinite loop if (config.shuffle) shuffleArray(assets); currentIndex = 0; } showPhoto(currentIndex); } function showPrevPhoto() { currentIndex--; if (currentIndex < 0) currentIndex = assets.length - 1; showPhoto(currentIndex); } function showPhoto(index) { if (!assets[index]) return; clearTimeout(slideshowTimer); clearInterval(progressTimer); var asset = assets[index]; var imageUrl = '/api/assets/' + asset.id + '/thumbnail?size=preview'; // Preload the image before showing var img = new Image(); img.onload = function () { displayImage(imageUrl, asset); }; img.onerror = function () { // Skip broken images setTimeout(showNextPhoto, 500); }; img.src = imageUrl; // Preload next image preloadNext(index + 1); } function displayImage(url, asset) { var fitStyle = config.imageFit || 'contain'; var incomingLayer, outgoingLayer; if (activeLayer === 'a') { incomingLayer = $layerB; outgoingLayer = $layerA; activeLayer = 'b'; } else { incomingLayer = $layerA; outgoingLayer = $layerB; activeLayer = 'a'; } // Set the image incomingLayer.style.backgroundImage = 'url(' + url + ')'; incomingLayer.style.backgroundSize = fitStyle; // Crossfade incomingLayer.classList.add('active'); outgoingLayer.classList.remove('active'); // Background blur if (config.backgroundBlur) { $bgBlur.style.backgroundImage = 'url(' + url + ')'; $bgBlur.classList.add('visible'); } // Update EXIF info updateExifInfo(asset); // Progress bar startProgress(); // Schedule next var interval = (config.slideshowInterval || 30) * 1000; slideshowTimer = setTimeout(showNextPhoto, interval); } function preloadNext(index) { if (index >= assets.length) index = 0; if (!assets[index]) return; var img = new Image(); img.src = '/api/assets/' + assets[index].id + '/thumbnail?size=preview'; } // --- EXIF Info --- function updateExifInfo(asset) { if (!config.showExif || !asset.exifInfo) { $exifInfo.textContent = ''; return; } var parts = []; var exif = asset.exifInfo; // Location var location = [exif.city, exif.state, exif.country].filter(Boolean).join(', '); if (location) parts.push('📍 ' + location); // Date if (exif.dateTimeOriginal) { var d = new Date(exif.dateTimeOriginal); parts.push(formatDate(d)); } else if (asset.fileCreatedAt) { var d2 = new Date(asset.fileCreatedAt); parts.push(formatDate(d2)); } // Camera if (exif.make || exif.model) { var camera = [exif.make, exif.model].filter(Boolean).join(' '); parts.push('📷 ' + camera); } $exifInfo.textContent = parts.join(' • '); } // --- Progress Bar --- function startProgress() { if (!config.showProgress) return; $progressFill.style.transition = 'none'; $progressFill.style.width = '0%'; progressStart = Date.now(); var duration = (config.slideshowInterval || 30) * 1000; // Force reflow $progressFill.offsetWidth; $progressFill.style.transition = 'width ' + duration + 'ms linear'; $progressFill.style.width = '100%'; } // --- Clock --- function updateClock() { var now = new Date(); if (config.showClock) { var h = now.getHours(); var m = now.getMinutes(); $clock.textContent = padZero(h) + ':' + padZero(m); } if (config.showDate) { var options = { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }; $dateDisplay.textContent = now.toLocaleDateString(undefined, options); } } // --- Overlay Toggle --- window.toggleOverlay = function () { overlayVisible = !overlayVisible; if (overlayVisible) { $overlay.classList.remove('hidden'); $btnSettings.classList.add('visible'); scheduleOverlayHide(); } else { $overlay.classList.add('hidden'); $btnSettings.classList.remove('visible'); } }; function scheduleOverlayHide() { clearTimeout(overlayTimeout); overlayTimeout = setTimeout(function () { $overlay.classList.add('hidden'); $btnSettings.classList.remove('visible'); overlayVisible = false; }, 8000); } // --- Navigation --- window.nextPhoto = function () { showNextPhoto(); if (overlayVisible) scheduleOverlayHide(); }; window.prevPhoto = function () { showPrevPhoto(); if (overlayVisible) scheduleOverlayHide(); }; // --- Keyboard Controls --- document.addEventListener('keydown', function (e) { if (!isRunning) return; switch (e.key) { case 'ArrowRight': case ' ': e.preventDefault(); nextPhoto(); break; case 'ArrowLeft': e.preventDefault(); prevPhoto(); break; case 'Escape': exitSlideshow(); break; case 'f': toggleFullscreen(); break; case 'i': toggleOverlay(); break; } }); // --- Fullscreen --- function toggleFullscreen() { if (!document.fullscreenElement && !document.webkitFullscreenElement) { var el = document.documentElement; if (el.requestFullscreen) el.requestFullscreen(); else if (el.webkitRequestFullscreen) el.webkitRequestFullscreen(); } else { if (document.exitFullscreen) document.exitFullscreen(); else if (document.webkitExitFullscreen) document.webkitExitFullscreen(); } } // --- Utility --- function shuffleArray(arr) { for (var i = arr.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } function padZero(n) { return n < 10 ? '0' + n : '' + n; } function formatDate(d) { var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; return d.getDate() + ' ' + months[d.getMonth()] + ' ' + d.getFullYear(); } function escapeHtml(str) { var div = document.createElement('div'); div.appendChild(document.createTextNode(str)); return div.innerHTML; } // --- Keep screen awake (where supported) --- async function requestWakeLock() { try { if ('wakeLock' in navigator) { await navigator.wakeLock.request('screen'); } } catch (e) { // Not supported or permission denied - that's fine } } document.addEventListener('visibilitychange', function () { if (document.visibilityState === 'visible' && isRunning) { requestWakeLock(); } }); // --- Prevent screen sleep on older devices via video trick --- function preventSleep() { try { var noSleep = document.createElement('video'); noSleep.setAttribute('playsinline', ''); noSleep.setAttribute('muted', ''); noSleep.setAttribute('loop', ''); noSleep.style.position = 'absolute'; noSleep.style.width = '1px'; noSleep.style.height = '1px'; noSleep.style.opacity = '0.01'; // Tiny transparent video data URI noSleep.src = 'data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAAAA' + 'htZGF0AAAA1m1vb3YAAABsbXZoZAAAAAAAAAAAAAAAAAAAA+gAAAAAAAEAAAEAAAAAAAAAAAAAAAABAAAAAAAAAAAA' + 'AAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAYdHJhawAAAFx0a2hk' + 'AAAAAwAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAA' + 'AAAAAAAAAAAAAAAAAJBtZGlhAAAAIG1kaGQAAAAAAAAAAAAAAAAAADIAAAAAAAAhhdmMxAAAAAAAAAAAAAAAAAAAAAAAA'; document.body.appendChild(noSleep); noSleep.play().catch(function () {}); } catch (e) {} } // --- Boot --- init(); requestWakeLock(); preventSleep(); })();