From d31abb68c8188618e659380d270d4c02eb35ab90 Mon Sep 17 00:00:00 2001 From: jessikitty Date: Tue, 19 May 2026 14:51:13 +1000 Subject: [PATCH] rebrand: rename ImmichFrame to Frambe in app.js header --- public/js/app.js | 470 ++++++++--------------------------------------- 1 file changed, 76 insertions(+), 394 deletions(-) diff --git a/public/js/app.js b/public/js/app.js index c5bfa2d..c33fc1c 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -1,4 +1,4 @@ -// === ImmichFrame - Frontend Application === +// === Frambe - Frontend Application === (function () { 'use strict'; @@ -37,519 +37,201 @@ 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 + if (!config.connected) { showError('API key not configured. Set IMMICH_API_KEY in your environment.'); return; } var serverRes = await fetch('/api/server-info'); var serverInfo = await serverRes.json(); - - if (!serverInfo.ok) { - showError('Cannot reach Immich server: ' + serverInfo.error); - return; - } - + 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); - } + 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; - } + 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; - } - + 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' - : ''; + var thumbUrl = a.albumThumbnailAssetId ? '/api/assets/' + a.albumThumbnailAssetId + '/thumbnail?size=thumbnail' : ''; html += '
'; - if (thumbUrl) { - html += ''; - } else { - html += '
📁
'; - } - html += '
'; - html += '
' + escapeHtml(a.albumName) + '
'; - html += '
' + a.assetCount + ' photos
'; - html += '
'; + html += thumbUrl ? '' : '
📁
'; + html += '
' + escapeHtml(a.albumName) + '
' + a.assetCount + ' photos
'; } $albumsList.innerHTML = html; - } catch (err) { - $albumsList.innerHTML = '

Failed to load albums

'; - } + } catch (err) { $albumsList.innerHTML = '

Failed to load albums

'; } } - // --- Source Selection --- window.selectSource = function (source) { - selectedSource = source; - selectedAlbumId = null; - - // Update button states + selectedSource = source; selectedAlbumId = null; 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'); - } - + 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 + selectedSource = 'album'; selectedAlbumId = albumId; 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'); - } + 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); - } + 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 + if (!assets.length) { $btnStart.textContent = 'No photos found'; setTimeout(function () { $btnStart.textContent = '▶ Start Slideshow'; $btnStart.disabled = false; }, 2000); return; } + $setupScreen.style.display = 'none'; $slideshowScreen.style.display = 'block'; + document.body.classList.remove('setup-mode'); isRunning = true; 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); - } + updateClock(); setInterval(updateClock, 1000); + currentIndex = -1; showNextPhoto(); 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'; + 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'); + $btnStart.textContent = '▶ Start Slideshow'; $btnStart.disabled = false; + $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 showNextPhoto() { currentIndex++; if (currentIndex >= assets.length) { 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); - + 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.onload = function () { displayImage(imageUrl, asset); }; + img.onerror = function () { 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 + if (activeLayer === 'a') { incomingLayer = $layerB; outgoingLayer = $layerA; activeLayer = 'b'; } + else { incomingLayer = $layerA; outgoingLayer = $layerB; activeLayer = 'a'; } 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 + incomingLayer.classList.add('active'); outgoingLayer.classList.remove('active'); + if (config.backgroundBlur) { $bgBlur.style.backgroundImage = 'url(' + url + ')'; $bgBlur.classList.add('visible'); } + updateExifInfo(asset); startProgress(); 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'; - } + 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 + if (!config.showExif || !asset.exifInfo) { $exifInfo.textContent = ''; return; } + var parts = [], exif = asset.exifInfo; 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); - } - + if (exif.dateTimeOriginal) { parts.push(formatDate(new Date(exif.dateTimeOriginal))); } + else if (asset.fileCreatedAt) { parts.push(formatDate(new Date(asset.fileCreatedAt))); } + if (exif.make || exif.model) { parts.push('📷 ' + [exif.make, exif.model].filter(Boolean).join(' ')); } $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.style.transition = 'none'; $progressFill.style.width = '0%'; + progressStart = Date.now(); var duration = (config.slideshowInterval || 30) * 1000; $progressFill.offsetWidth; - - $progressFill.style.transition = 'width ' + duration + 'ms linear'; - $progressFill.style.width = '100%'; + $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); - } + if (config.showClock) { $clock.textContent = padZero(now.getHours()) + ':' + padZero(now.getMinutes()); } + if (config.showDate) { $dateDisplay.textContent = now.toLocaleDateString(undefined, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }); } } - // --- 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'); - } + 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); - } + 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.nextPhoto = function () { showNextPhoto(); if (overlayVisible) scheduleOverlayHide(); }; + window.prevPhoto = function () { showPrevPhoto(); 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; + 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(); - } + 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 shuffleArray(arr) { for (var i = arr.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var t = arr[i]; arr[i] = arr[j]; arr[j] = t; } } + function padZero(n) { return n < 10 ? '0' + n : '' + n; } + function formatDate(d) { var m = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; return d.getDate() + ' ' + m[d.getMonth()] + ' ' + d.getFullYear(); } + function escapeHtml(s) { var d = document.createElement('div'); d.appendChild(document.createTextNode(s)); return d.innerHTML; } - function padZero(n) { - return n < 10 ? '0' + n : '' + n; - } + async function requestWakeLock() { try { if ('wakeLock' in navigator) await navigator.wakeLock.request('screen'); } catch (e) {} } + document.addEventListener('visibilitychange', function () { if (document.visibilityState === 'visible' && isRunning) requestWakeLock(); }); - 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) {} + var v = document.createElement('video'); v.setAttribute('playsinline',''); v.setAttribute('muted',''); v.setAttribute('loop',''); + v.style.cssText = 'position:absolute;width:1px;height:1px;opacity:0.01'; + v.src = 'data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAAAAhtZGF0AAAA1m1vb3YAAABsbXZoZAAAAAAAAAAAAAAAAAAAA+gAAAAAAAEAAAEAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAYdHJhawAAAFx0a2hkAAAAAwAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAJBtZGlhAAAAIG1kaGQAAAAAAAAAAAAAAAAAADIAAAAAAAAhhdmMxAAAAAAAAAAAAAAAAAAAAAAAA'; + document.body.appendChild(v); v.play().catch(function(){}); + } catch(e){} } - // --- Boot --- - init(); - requestWakeLock(); - preventSleep(); + init(); requestWakeLock(); preventSleep(); })();