feat: canvas-based cumulative photo pile, unified polaroid for photos+videos, larger main frame

This commit is contained in:
2026-05-20 09:48:15 +10:00
parent 65993839a7
commit 804c6cfd86
+101 -39
View File
@@ -1,10 +1,11 @@
// === Frambe v1.3.0 - Vintage Polaroid Pile === // === Frambe v1.3.0 - Vintage Polaroid Pile (Canvas) ===
(function () { (function () {
'use strict'; 'use strict';
var config = {}, assets = [], currentIndex = -1, slideshowTimer = null; var config = {}, assets = [], currentIndex = -1, slideshowTimer = null;
var overlayVisible = true, overlayTimeout = null, selectedSource = null, selectedAlbumId = null; var overlayVisible = true, overlayTimeout = null, selectedSource = null, selectedAlbumId = null;
var selectedPersonId = null, isRunning = false, refreshTimer = null, urlDriven = false; var selectedPersonId = null, isRunning = false, refreshTimer = null, urlDriven = false;
var pileHistory = [], PILE_SIZE = 5, currentVideoPlaying = false; var currentVideoPlaying = false;
var pileCanvas, pileCtx; // canvas for accumulated polaroid pile
var $setupScreen = document.getElementById('setup-screen'), $slideshowScreen = document.getElementById('slideshow-screen'); var $setupScreen = document.getElementById('setup-screen'), $slideshowScreen = document.getElementById('slideshow-screen');
var $connectionStatus = document.getElementById('connection-status'), $setupContent = document.getElementById('setup-content'); var $connectionStatus = document.getElementById('connection-status'), $setupContent = document.getElementById('setup-content');
@@ -17,15 +18,8 @@
var $overlay = document.getElementById('overlay'), $btnSettings = document.getElementById('btn-settings'); var $overlay = document.getElementById('overlay'), $btnSettings = document.getElementById('btn-settings');
var $progressBar = document.getElementById('progress-bar'); var $progressBar = document.getElementById('progress-bar');
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 }
];
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; } 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; }
async function autoLaunch(src, aid, pid) { urlDriven=true;selectedSource=src;selectedAlbumId=aid||null;selectedPersonId=pid||null;console.log('[Frambe] Auto-launching: source='+src+(aid?' album='+aid:'')+(pid?' person='+pid:''));await doStartSlideshow(); }
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(); }
async function init() { async function init() {
document.body.classList.add('setup-mode'); document.body.classList.add('setup-mode');
@@ -49,16 +43,86 @@
} }
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; }
async function loadAlbums() { try { var albums=await(await fetch('/api/albums')).json();if(!albums.length){$albumsList.innerHTML='<p class="loading-text">No albums found</p>';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+='<div class="album-item" data-id="'+a.id+'" onclick="selectAlbum(\''+a.id+'\', this)">';html+=thu?'<img class="album-thumb" src="'+thu+'" alt="" loading="lazy">':'<div class="album-thumb" style="display:flex;align-items:center;justify-content:center;font-size:1.2rem">📁</div>';html+='<div class="album-info"><div class="album-name">'+escapeHtml(a.albumName)+'</div><div class="album-count">'+a.assetCount+' items</div></div></div>';}$albumsList.innerHTML=html;}catch(e){$albumsList.innerHTML='<p class="loading-text">Failed to load albums</p>';} } async function loadAlbums() { try { var albums=await(await fetch('/api/albums')).json();if(!albums.length){$albumsList.innerHTML='<p class="loading-text">No albums found</p>';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+='<div class="album-item" data-id="'+a.id+'" onclick="selectAlbum(\''+a.id+'\', this)">';html+=thu?'<img class="album-thumb" src="'+thu+'" alt="" loading="lazy">':'<div class="album-thumb" style="display:flex;align-items:center;justify-content:center;font-size:1.2rem">📁</div>';html+='<div class="album-info"><div class="album-name">'+escapeHtml(a.albumName)+'</div><div class="album-count">'+a.assetCount+' items</div></div></div>';}$albumsList.innerHTML=html;}catch(e){$albumsList.innerHTML='<p class="loading-text">Failed to load albums</p>';} }
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.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.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;};
async function loadAssets() { var res;if(selectedSource==='album'&&selectedAlbumId){res=await fetch('/api/albums/'+selectedAlbumId);if(!res.ok)throw new Error('Album fetch failed: '+res.status);var al=await res.json();assets=al.assets||[];}else if(selectedSource==='person'&&selectedPersonId){res=await fetch('/api/people/'+selectedPersonId);if(!res.ok)throw new Error('Person fetch failed: '+res.status);assets=await res.json();}else if(selectedSource==='favorites'){res=await fetch('/api/assets/favorites');if(!res.ok)throw new Error('Favorites fetch failed: '+res.status);assets=await res.json();}else{res=await fetch('/api/assets/random?count=100');if(!res.ok)throw new Error('Random fetch failed: '+res.status);assets=await res.json();}if(config.shuffle)shuffleArray(assets);console.log('[Frambe] Loaded '+assets.length+' assets'); } async function loadAssets() { var res;if(selectedSource==='album'&&selectedAlbumId){res=await fetch('/api/albums/'+selectedAlbumId);if(!res.ok)throw new Error('Album fetch failed: '+res.status);var al=await res.json();assets=al.assets||[];}else if(selectedSource==='person'&&selectedPersonId){res=await fetch('/api/people/'+selectedPersonId);if(!res.ok)throw new Error('Person fetch failed: '+res.status);assets=await res.json();}else if(selectedSource==='favorites'){res=await fetch('/api/assets/favorites');if(!res.ok)throw new Error('Favorites fetch failed: '+res.status);assets=await res.json();}else{res=await fetch('/api/assets/random?count=100');if(!res.ok)throw new Error('Random fetch failed: '+res.status);assets=await res.json();}if(config.shuffle)shuffleArray(assets);console.log('[Frambe] Loaded '+assets.length+' assets'); }
function startRefreshTimer() { if(refreshTimer)clearInterval(refreshTimer);refreshTimer=setInterval(async function(){try{var oldIds={};for(var i=0;i<assets.length;i++)oldIds[assets[i].id]=true;var nw,r;if(selectedSource==='album'&&selectedAlbumId){r=await(await fetch('/api/albums/'+selectedAlbumId)).json();nw=r.assets||[];}else if(selectedSource==='person'&&selectedPersonId){nw=await(await fetch('/api/people/'+selectedPersonId)).json();}else if(selectedSource==='favorites'){nw=await(await fetch('/api/assets/favorites')).json();}else return;var added=0;for(var j=0;j<nw.length;j++){if(!oldIds[nw[j].id]){assets.push(nw[j]);added++;}}if(added>0)console.log('[Frambe] Refresh added '+added+' new asset(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;i<assets.length;i++)oldIds[assets[i].id]=true;var nw,r;if(selectedSource==='album'&&selectedAlbumId){r=await(await fetch('/api/albums/'+selectedAlbumId)).json();nw=r.assets||[];}else if(selectedSource==='person'&&selectedPersonId){nw=await(await fetch('/api/people/'+selectedPersonId)).json();}else if(selectedSource==='favorites'){nw=await(await fetch('/api/assets/favorites')).json();}else return;var added=0;for(var j=0;j<nw.length;j++){if(!oldIds[nw[j].id]){assets.push(nw[j]);added++;}}if(added>0)console.log('[Frambe] Refresh added '+added+' new asset(s)');}catch(e){console.warn('[Frambe] Refresh failed: '+e.message);}}, (config.refreshInterval||300)*1000); }
// =========================
// CANVAS PILE
// =========================
function initPileCanvas() {
pileCanvas = document.getElementById('pile-canvas');
pileCanvas.width = window.innerWidth * (window.devicePixelRatio || 1);
pileCanvas.height = window.innerHeight * (window.devicePixelRatio || 1);
pileCanvas.style.width = window.innerWidth + 'px';
pileCanvas.style.height = window.innerHeight + 'px';
pileCtx = pileCanvas.getContext('2d');
pileCtx.scale(window.devicePixelRatio || 1, window.devicePixelRatio || 1);
}
function clearPileCanvas() {
if (pileCtx) {
pileCtx.setTransform(1,0,0,1,0,0);
pileCtx.clearRect(0, 0, pileCanvas.width, pileCanvas.height);
pileCtx.scale(window.devicePixelRatio || 1, window.devicePixelRatio || 1);
}
}
function dropPhotoPile(imgSrc) {
var img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function () {
var vw = window.innerWidth, vh = window.innerHeight;
// Polaroid size: ~18-25% of screen width
var polaroidW = vw * (0.18 + Math.random() * 0.07);
var padding = polaroidW * 0.04;
var bottomPad = polaroidW * 0.12;
var innerW = polaroidW - padding * 2;
var innerH = innerW * (img.height / img.width);
var totalH = innerH + padding + bottomPad;
// Random position across the screen
var cx = Math.random() * vw;
var cy = Math.random() * vh;
var rot = (Math.random() - 0.5) * 30; // -15 to +15 degrees
pileCtx.save();
pileCtx.translate(cx, cy);
pileCtx.rotate(rot * Math.PI / 180);
// Shadow
pileCtx.shadowColor = 'rgba(0,0,0,0.5)';
pileCtx.shadowBlur = 15;
pileCtx.shadowOffsetX = 3;
pileCtx.shadowOffsetY = 5;
// White polaroid border
pileCtx.fillStyle = '#f0ebe3';
pileCtx.fillRect(-polaroidW/2, -totalH/2, polaroidW, totalH);
// Clear shadow for the image
pileCtx.shadowColor = 'transparent';
pileCtx.shadowBlur = 0;
pileCtx.shadowOffsetX = 0;
pileCtx.shadowOffsetY = 0;
// Photo inside
pileCtx.drawImage(img, -polaroidW/2 + padding, -totalH/2 + padding, innerW, innerH);
pileCtx.restore();
console.log('[Frambe] Dropped polaroid onto pile');
};
img.onerror = function () { console.warn('[Frambe] Pile image failed to load'); };
img.src = imgSrc;
}
// =========================
// SLIDESHOW ENGINE
// =========================
async function doStartSlideshow() { async function doStartSlideshow() {
if (!selectedSource) return; if (!selectedSource) return;
$btnStart.disabled = true; $btnStart.innerHTML = '<span class="spinner"></span> Loading…'; $btnStart.disabled = true; $btnStart.innerHTML = '<span class="spinner"></span> Loading…';
@@ -67,25 +131,26 @@
if (!assets.length) { $btnStart.textContent = 'No photos found'; setTimeout(function(){$btnStart.textContent='▶ Start Slideshow';$btnStart.disabled=false;},2000); return; } 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'; $setupScreen.style.display = 'none'; $slideshowScreen.style.display = 'block';
document.body.classList.remove('setup-mode'); isRunning = true; document.body.classList.remove('setup-mode'); isRunning = true;
initPileCanvas();
if (!config.showClock) $clock.style.display = 'none'; if (!config.showClock) $clock.style.display = 'none';
if (!config.showDate) $dateDisplay.style.display = 'none'; if (!config.showDate) $dateDisplay.style.display = 'none';
if (!config.showExif) $exifInfo.style.display = 'none'; if (!config.showExif) $exifInfo.style.display = 'none';
if (!config.showProgress) $progressBar.style.display = 'none'; if (!config.showProgress) $progressBar.style.display = 'none';
if (!config.backgroundBlur) $bgBlur.style.display = 'none'; if (!config.backgroundBlur) $bgBlur.style.display = 'none';
updateClock(); setInterval(updateClock, 1000); updateClock(); setInterval(updateClock, 1000);
currentIndex = -1; pileHistory = []; currentIndex = -1;
showNextAsset(); scheduleOverlayHide(); startRefreshTimer(); 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); } } catch (err) { console.error('[Frambe] Start failed: '+err.message); $btnStart.textContent='Error: '+err.message; setTimeout(function(){$btnStart.textContent='▶ Start Slideshow';$btnStart.disabled=false;},3000); }
} }
window.startSlideshow = function () { doStartSlideshow(); }; window.startSlideshow = function () { doStartSlideshow(); };
window.exitSlideshow = function () { window.exitSlideshow = function () {
if (urlDriven) { window.location.href = window.location.pathname; return; } if (urlDriven) { window.location.href = window.location.pathname; return; }
isRunning=false;clearTimeout(slideshowTimer);if(refreshTimer)clearInterval(refreshTimer);stopVideo(); isRunning=false;clearTimeout(slideshowTimer);if(refreshTimer)clearInterval(refreshTimer);stopVideo();
$slideshowScreen.style.display='none';$setupScreen.style.display='flex';document.body.classList.add('setup-mode'); $slideshowScreen.style.display='none';$setupScreen.style.display='flex';document.body.classList.add('setup-mode');
$btnStart.textContent='▶ Start Slideshow';$btnStart.disabled=false; $btnStart.textContent='▶ Start Slideshow';$btnStart.disabled=false;
$bgBlur.style.backgroundImage='';$bgBlur.classList.remove('visible');$mainFrame.classList.remove('visible');clearPile(); $bgBlur.style.backgroundImage='';$bgBlur.classList.remove('visible');$mainFrame.classList.remove('visible');
clearPileCanvas();
}; };
function showNextAsset() { currentIndex++;if(currentIndex>=assets.length){if(config.shuffle)shuffleArray(assets);currentIndex=0;}showAsset(currentIndex); } function showNextAsset() { currentIndex++;if(currentIndex>=assets.length){if(config.shuffle)shuffleArray(assets);currentIndex=0;}showAsset(currentIndex); }
@@ -97,53 +162,50 @@
var asset = assets[index], isVideo = asset.type === 'VIDEO'; var asset = assets[index], isVideo = asset.type === 'VIDEO';
var thumbUrl = '/api/assets/' + asset.id + '/thumbnail?size=preview'; var thumbUrl = '/api/assets/' + asset.id + '/thumbnail?size=preview';
console.log('[Frambe] Showing ' + (isVideo ? 'VIDEO' : 'PHOTO') + ': ' + (asset.originalFileName || asset.id)); 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]); }
// Drop previous photo onto the pile canvas
if (currentIndex > 0 || pileCanvas) {
var pi = currentIndex - 1; if (pi < 0) pi = assets.length - 1;
if (assets[pi]) { dropPhotoPile('/api/assets/' + assets[pi].id + '/thumbnail?size=thumbnail'); }
}
// Fade out main
$mainFrame.classList.remove('visible'); $mainFrame.classList.remove('visible');
var img = new Image(); var img = new Image();
img.onload = function () { setTimeout(function () { displayAsset(asset, thumbUrl, isVideo); }, 400); }; img.onload = function () { setTimeout(function () { displayAsset(asset, thumbUrl, isVideo); }, 400); };
img.onerror = function () { setTimeout(showNextAsset, 500); }; img.onerror = function () { setTimeout(showNextAsset, 500); };
img.src = thumbUrl; 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'; }
// Preload next
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 displayAsset(asset, thumbUrl, isVideo) { function displayAsset(asset, thumbUrl, isVideo) {
if (config.backgroundBlur) { $bgBlur.style.backgroundImage = 'url(' + thumbUrl + ')'; $bgBlur.classList.add('visible'); } if (config.backgroundBlur) { $bgBlur.style.backgroundImage = 'url(' + thumbUrl + ')'; $bgBlur.classList.add('visible'); }
// Always polaroid style
$mainVideo.style.display = 'none'; $mainPhoto.style.display = 'none';
if (isVideo) { if (isVideo) {
$mainFrame.classList.remove('polaroid'); $mainFrame.classList.add('filmstrip'); $mainVideo.style.display = 'block';
$mainPhoto.style.display = 'none'; $mainVideo.style.display = 'block';
$mainVideo.src = '/api/assets/' + asset.id + '/video'; $mainVideo.poster = thumbUrl; $mainVideo.src = '/api/assets/' + asset.id + '/video'; $mainVideo.poster = thumbUrl;
$mainVideo.load(); $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.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(); }; $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();} }, Math.max((config.slideshowInterval||30)*3, 120)*1000);
slideshowTimer = setTimeout(function(){ if(currentVideoPlaying){console.log('[Frambe] Video timeout');showNextAsset();} }, maxDur);
} else { } else {
$mainFrame.classList.remove('filmstrip'); $mainFrame.classList.add('polaroid'); $mainPhoto.style.display = 'block'; $mainPhoto.src = thumbUrl;
$mainVideo.style.display = 'none'; $mainPhoto.style.display = 'block'; $mainPhoto.src = thumbUrl;
slideshowTimer = setTimeout(showNextAsset, (config.slideshowInterval||30)*1000); slideshowTimer = setTimeout(showNextAsset, (config.slideshowInterval||30)*1000);
} }
requestAnimationFrame(function () { $mainFrame.classList.add('visible'); }); requestAnimationFrame(function () { $mainFrame.classList.add('visible'); });
updateExifInfo(asset); startProgress(isVideo ? null : (config.slideshowInterval||30)*1000); updateExifInfo(asset); startProgress(isVideo ? null : (config.slideshowInterval||30)*1000);
} }
function stopVideo() { if(currentVideoPlaying||$mainVideo.src){try{$mainVideo.pause();}catch(e){}$mainVideo.removeAttribute('src');$mainVideo.load();$mainVideo.onended=null;currentVideoPlaying=false;} } 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 = '<div style="width:100%;height:100%;background:url(/api/assets/'+aid+'/thumbnail?size=thumbnail) center/cover;background-color:#d4cfc5;"></div>';
$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 clearPile() { pileHistory=[];for(var i=0;i<PILE_SIZE;i++)document.getElementById('pile-'+i).classList.remove('visible'); }
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(' '));if(a.type==='VIDEO')p.push('Video');$exifInfo.textContent=p.join(' · '); } 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(' '));if(a.type==='VIDEO')p.push('Video');$exifInfo.textContent=p.join(' · '); }
function startProgress(ms) { if(!config.showProgress)return;$progressFill.style.transition='none';$progressFill.style.width='0%';$progressFill.offsetWidth;if(ms){$progressFill.style.transition='width '+ms+'ms linear';$progressFill.style.width='100%';} } function startProgress(ms) { if(!config.showProgress)return;$progressFill.style.transition='none';$progressFill.style.width='0%';$progressFill.offsetWidth;if(ms){$progressFill.style.transition='width '+ms+'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'}); } 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'}); }