From 0f4a995cdcb5ad23b83c73a2f278645dcb07fe73 Mon Sep 17 00:00:00 2001
From: jessikitty
Date: Tue, 19 May 2026 21:48:53 +1000
Subject: [PATCH] feat(1.3.0): Polaroid pile slideshow engine, video playback
with filmstrip frame, floating animation
---
public/js/app.js | 280 ++++++++++++++++++-----------------------------
1 file changed, 104 insertions(+), 176 deletions(-)
diff --git a/public/js/app.js b/public/js/app.js
index 0dd9ba2..41bc6fe 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -1,236 +1,164 @@
-// === Frambe - Frontend Application ===
+// === Frambe v1.3.0 - Vintage Polaroid Pile ===
(function () {
'use strict';
- var config = {}, assets = [], currentIndex = -1, activeLayer = 'a', slideshowTimer = null;
+ var config = {}, assets = [], currentIndex = -1, slideshowTimer = null;
var overlayVisible = true, overlayTimeout = null, selectedSource = null, selectedAlbumId = null;
var selectedPersonId = null, isRunning = false, refreshTimer = null, urlDriven = false;
+ var pileHistory = [], PILE_SIZE = 5, currentVideoPlaying = false;
var $setupScreen = document.getElementById('setup-screen'), $slideshowScreen = document.getElementById('slideshow-screen');
var $connectionStatus = document.getElementById('connection-status'), $setupContent = document.getElementById('setup-content');
var $setupError = document.getElementById('setup-error'), $errorDetail = document.getElementById('error-detail');
var $albumsList = document.getElementById('albums-list'), $btnStart = document.getElementById('btn-start');
- var $layerA = document.getElementById('photo-layer-a'), $layerB = document.getElementById('photo-layer-b');
- var $bgBlur = document.getElementById('bg-blur'), $clock = document.getElementById('clock');
- var $dateDisplay = document.getElementById('date-display'), $exifInfo = document.getElementById('exif-info');
- var $progressFill = document.getElementById('progress-fill'), $overlay = document.getElementById('overlay');
- var $btnSettings = document.getElementById('btn-settings'), $progressBar = document.getElementById('progress-bar');
+ var $bgBlur = document.getElementById('bg-blur'), $mainFrame = document.getElementById('main-frame');
+ var $mainPhoto = document.getElementById('main-photo'), $mainVideo = document.getElementById('main-video');
+ var $clock = document.getElementById('clock'), $dateDisplay = document.getElementById('date-display');
+ var $exifInfo = document.getElementById('exif-info'), $progressFill = document.getElementById('progress-fill');
+ var $overlay = document.getElementById('overlay'), $btnSettings = document.getElementById('btn-settings');
+ var $progressBar = document.getElementById('progress-bar');
- function getUrlParams() {
- var p = {}, s = window.location.search.substring(1); if (!s) return p;
- var pairs = s.split('&');
- for (var i = 0; i < pairs.length; i++) { var kv = pairs[i].split('='); p[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1] || ''); }
- return p;
- }
+ var pilePositions = [
+ { x: -18, y: -12, rot: -11, scale: 0.85 }, { x: 22, y: -8, rot: 7, scale: 0.80 },
+ { x: -14, y: 15, rot: 14, scale: 0.78 }, { x: 20, y: 12, rot: -9, scale: 0.82 },
+ { x: -5, y: -20, rot: 4, scale: 0.75 }
+ ];
- // --- Auto-launch helper: sets source and immediately starts slideshow ---
- async function autoLaunch(source, albumId, personId) {
- urlDriven = true;
- selectedSource = source;
- selectedAlbumId = albumId || null;
- selectedPersonId = personId || null;
- console.log('Frambe: auto-launching source=' + source + (albumId ? ' album=' + albumId : '') + (personId ? ' person=' + personId : ''));
- await doStartSlideshow();
- }
+ function getUrlParams() { var p={},s=window.location.search.substring(1);if(!s)return p;var pairs=s.split('&');for(var i=0;iNo albums found
'; return; }
- var html = '';
- for (var i = 0; i < albums.length; i++) {
- var a = albums[i], thu = a.albumThumbnailAssetId ? '/api/assets/' + a.albumThumbnailAssetId + '/thumbnail?size=thumbnail' : '';
- html += '';
- html += thu ? '

' : '
📁
';
- html += '
' + escapeHtml(a.albumName) + '
' + a.assetCount + ' photos
';
- }
- $albumsList.innerHTML = html;
- } catch (e) { $albumsList.innerHTML = 'Failed to load albums
'; }
- }
+ async function loadAlbums() { try { var albums=await(await fetch('/api/albums')).json();if(!albums.length){$albumsList.innerHTML='No albums found
';return;}var html='';for(var i=0;i';html+=thu?'
':'📁
';html+=''+escapeHtml(a.albumName)+'
'+a.assetCount+' items
';}$albumsList.innerHTML=html;}catch(e){$albumsList.innerHTML='Failed to load albums
';} }
- window.selectSource = function (src) {
- selectedSource = src; selectedAlbumId = null; selectedPersonId = null;
- document.getElementById('btn-all-photos').classList.toggle('selected', src === 'random');
- document.getElementById('btn-favorites').classList.toggle('selected', src === 'favorites');
- var items = document.querySelectorAll('.album-item');
- for (var i = 0; i < items.length; i++) items[i].classList.remove('selected');
- $btnStart.disabled = false;
- };
- window.selectAlbum = function (id, el) {
- selectedSource = 'album'; selectedAlbumId = id; selectedPersonId = null;
- document.getElementById('btn-all-photos').classList.remove('selected');
- document.getElementById('btn-favorites').classList.remove('selected');
- var items = document.querySelectorAll('.album-item');
- for (var i = 0; i < items.length; i++) items[i].classList.remove('selected');
- el.classList.add('selected'); $btnStart.disabled = false;
- };
+ window.selectSource = function(src){selectedSource=src;selectedAlbumId=null;selectedPersonId=null;document.getElementById('btn-all-photos').classList.toggle('selected',src==='random');document.getElementById('btn-favorites').classList.toggle('selected',src==='favorites');var items=document.querySelectorAll('.album-item');for(var i=0;i 0) console.log('Frambe: added ' + added + ' new photo(s)');
- } catch (e) { console.warn('Frambe: refresh failed', e.message); }
- }, (config.refreshInterval || 300) * 1000);
- }
+ function startRefreshTimer() { if(refreshTimer)clearInterval(refreshTimer);refreshTimer=setInterval(async function(){try{var oldIds={};for(var i=0;i0)console.log('[Frambe] Refresh added '+added+' new asset(s)');}catch(e){console.warn('[Frambe] Refresh failed: '+e.message);}}, (config.refreshInterval||300)*1000); }
- // --- Core slideshow start (used by both button click and auto-launch) ---
async function doStartSlideshow() {
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 view
- $setupScreen.style.display = 'none';
- $slideshowScreen.style.display = 'block';
- document.body.classList.remove('setup-mode');
- isRunning = true;
- var t = (config.transitionDuration || 2) * 1000;
- $layerA.style.transition = 'opacity ' + t + 'ms ease';
- $layerB.style.transition = 'opacity ' + t + 'ms ease';
- $bgBlur.style.transition = 'opacity ' + (t * 0.75) + 'ms ease';
+ 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;
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';
updateClock(); setInterval(updateClock, 1000);
- currentIndex = -1;
- showNextPhoto();
- scheduleOverlayHide();
- startRefreshTimer();
- } catch (err) {
- console.error('Frambe: slideshow start failed', err);
- $btnStart.textContent = 'Error: ' + err.message;
- setTimeout(function () { $btnStart.textContent = '▶ Start Slideshow'; $btnStart.disabled = false; }, 3000);
- }
+ currentIndex = -1; pileHistory = [];
+ showNextAsset(); scheduleOverlayHide(); startRefreshTimer();
+ } catch (err) { console.error('[Frambe] Start failed: '+err.message); $btnStart.textContent='Error: '+err.message; setTimeout(function(){$btnStart.textContent='▶ Start Slideshow';$btnStart.disabled=false;},3000); }
}
- // Exposed for the button onclick
window.startSlideshow = function () { doStartSlideshow(); };
window.exitSlideshow = function () {
if (urlDriven) { window.location.href = window.location.pathname; return; }
- isRunning = false; clearTimeout(slideshowTimer); if (refreshTimer) clearInterval(refreshTimer);
- $slideshowScreen.style.display = 'none'; $setupScreen.style.display = 'flex'; document.body.classList.add('setup-mode');
- $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';
+ isRunning=false;clearTimeout(slideshowTimer);if(refreshTimer)clearInterval(refreshTimer);stopVideo();
+ $slideshowScreen.style.display='none';$setupScreen.style.display='flex';document.body.classList.add('setup-mode');
+ $btnStart.textContent='▶ Start Slideshow';$btnStart.disabled=false;
+ $bgBlur.style.backgroundImage='';$bgBlur.classList.remove('visible');$mainFrame.classList.remove('visible');clearPile();
};
- 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(idx) {
- if (!assets[idx]) return; clearTimeout(slideshowTimer);
- var a = assets[idx], url = '/api/assets/' + a.id + '/thumbnail?size=preview';
- var img = new Image(); img.onload = function () { displayImage(url, a); }; img.onerror = function () { setTimeout(showNextPhoto, 500); }; img.src = url;
- preloadNext(idx + 1);
+ function showNextAsset() { currentIndex++;if(currentIndex>=assets.length){if(config.shuffle)shuffleArray(assets);currentIndex=0;}showAsset(currentIndex); }
+ function showPrevAsset() { currentIndex--;if(currentIndex<0)currentIndex=assets.length-1;showAsset(currentIndex); }
+
+ function showAsset(index) {
+ if (!assets[index]) return;
+ clearTimeout(slideshowTimer); stopVideo();
+ var asset = assets[index], isVideo = asset.type === 'VIDEO';
+ var thumbUrl = '/api/assets/' + asset.id + '/thumbnail?size=preview';
+ console.log('[Frambe] Showing ' + (isVideo ? 'VIDEO' : 'PHOTO') + ': ' + (asset.originalFileName || asset.id));
+ if (pileHistory.length > 0 || currentIndex > 0) { var pi = currentIndex - 1; if (pi < 0) pi = assets.length - 1; if (assets[pi]) addToPile(assets[pi]); }
+ $mainFrame.classList.remove('visible');
+ var img = new Image();
+ img.onload = function () { setTimeout(function () { displayAsset(asset, thumbUrl, isVideo); }, 400); };
+ img.onerror = function () { setTimeout(showNextAsset, 500); };
+ img.src = thumbUrl;
+ var ni = index + 1; if (ni >= assets.length) ni = 0; if (assets[ni]) { var pre = new Image(); pre.src = '/api/assets/' + assets[ni].id + '/thumbnail?size=preview'; }
}
- function displayImage(url, asset) {
- var fit = config.imageFit || 'contain', inc, out;
- if (activeLayer === 'a') { inc = $layerB; out = $layerA; activeLayer = 'b'; } else { inc = $layerA; out = $layerB; activeLayer = 'a'; }
- inc.style.backgroundImage = 'url(' + url + ')'; inc.style.backgroundSize = fit;
- inc.classList.add('active'); out.classList.remove('active');
- if (config.backgroundBlur) { $bgBlur.style.backgroundImage = 'url(' + url + ')'; $bgBlur.classList.add('visible'); }
- updateExifInfo(asset); startProgress();
- slideshowTimer = setTimeout(showNextPhoto, (config.slideshowInterval || 30) * 1000);
+
+ function displayAsset(asset, thumbUrl, isVideo) {
+ if (config.backgroundBlur) { $bgBlur.style.backgroundImage = 'url(' + thumbUrl + ')'; $bgBlur.classList.add('visible'); }
+ if (isVideo) {
+ $mainFrame.classList.remove('polaroid'); $mainFrame.classList.add('filmstrip');
+ $mainPhoto.style.display = 'none'; $mainVideo.style.display = 'block';
+ $mainVideo.src = '/api/assets/' + asset.id + '/video'; $mainVideo.poster = thumbUrl;
+ $mainVideo.load();
+ $mainVideo.play().then(function(){ currentVideoPlaying=true; console.log('[Frambe] Video playing'); }).catch(function(e){ console.warn('[Frambe] Video autoplay failed: '+e.message); });
+ $mainVideo.onended = function () { console.log('[Frambe] Video ended'); currentVideoPlaying=false; showNextAsset(); };
+ var maxDur = Math.max((config.slideshowInterval||30)*3, 120)*1000;
+ slideshowTimer = setTimeout(function(){ if(currentVideoPlaying){console.log('[Frambe] Video timeout');showNextAsset();} }, maxDur);
+ } else {
+ $mainFrame.classList.remove('filmstrip'); $mainFrame.classList.add('polaroid');
+ $mainVideo.style.display = 'none'; $mainPhoto.style.display = 'block'; $mainPhoto.src = thumbUrl;
+ slideshowTimer = setTimeout(showNextAsset, (config.slideshowInterval||30)*1000);
+ }
+ requestAnimationFrame(function () { $mainFrame.classList.add('visible'); });
+ updateExifInfo(asset); startProgress(isVideo ? null : (config.slideshowInterval||30)*1000);
}
- function preloadNext(i) { if (i >= assets.length) i = 0; if (!assets[i]) return; var img = new Image(); img.src = '/api/assets/' + assets[i].id + '/thumbnail?size=preview'; }
- function updateExifInfo(a) {
- if (!config.showExif || !a.exifInfo) { $exifInfo.textContent = ''; return; }
- var p = [], e = a.exifInfo, loc = [e.city, e.state, e.country].filter(Boolean).join(', ');
- if (loc) p.push('📍 ' + loc);
- if (e.dateTimeOriginal) p.push(formatDate(new Date(e.dateTimeOriginal)));
- else if (a.fileCreatedAt) p.push(formatDate(new Date(a.fileCreatedAt)));
- if (e.make || e.model) p.push('📷 ' + [e.make, e.model].filter(Boolean).join(' '));
- $exifInfo.textContent = p.join(' • ');
+
+ function stopVideo() { if(currentVideoPlaying||$mainVideo.src){try{$mainVideo.pause();}catch(e){}$mainVideo.removeAttribute('src');$mainVideo.load();$mainVideo.onended=null;currentVideoPlaying=false;} }
+
+ function addToPile(asset) {
+ pileHistory.push(asset.id); if (pileHistory.length > PILE_SIZE) pileHistory.shift();
+ for (var i = 0; i < PILE_SIZE; i++) {
+ var $pile = document.getElementById('pile-' + i), $inner = $pile.querySelector('.pile-inner');
+ var histIdx = pileHistory.length - 1 - i;
+ if (histIdx >= 0 && pileHistory[histIdx]) {
+ var aid = pileHistory[histIdx], pos = pilePositions[i];
+ $inner.style.cssText = 'width:100%;height:100%;background-color:#f5f0e8;padding:4% 4% 14% 4%;box-shadow:0 2px 15px rgba(0,0,0,0.4),0 0 1px rgba(0,0,0,0.2);border-radius:2px;';
+ $inner.innerHTML = '';
+ $pile.style.transform = 'translate(calc(-50% + '+pos.x+'vmin), calc(-50% + '+pos.y+'vmin)) rotate('+pos.rot+'deg) scale('+pos.scale+')';
+ $pile.style.opacity = 0.35 - (i * 0.04); $pile.classList.add('visible');
+ } else { $pile.classList.remove('visible'); }
+ }
}
- function startProgress() {
- if (!config.showProgress) return;
- $progressFill.style.transition = 'none'; $progressFill.style.width = '0%'; $progressFill.offsetWidth;
- $progressFill.style.transition = 'width ' + ((config.slideshowInterval || 30) * 1000) + 'ms linear'; $progressFill.style.width = '100%';
- }
- function updateClock() {
- var n = new Date();
- if (config.showClock) $clock.textContent = padZero(n.getHours()) + ':' + padZero(n.getMinutes());
- if (config.showDate) $dateDisplay.textContent = n.toLocaleDateString(undefined, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
- }
- 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); }
- window.nextPhoto = function () { showNextPhoto(); if (overlayVisible) scheduleOverlayHide(); };
- window.prevPhoto = function () { showPrevPhoto(); if (overlayVisible) scheduleOverlayHide(); };
- 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; } });
- 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(); } }
- 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; }
- 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 preventSleep() { try { 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){} }
- init(); requestWakeLock(); preventSleep();
+ function clearPile() { pileHistory=[];for(var i=0;i0;i--){var j=Math.floor(Math.random()*(i+1));var t=a[i];a[i]=a[j];a[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;}
+ 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 preventSleep(){try{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){}}
+ init();requestWakeLock();preventSleep();
})();