rebrand: rename ImmichFrame to Frambe in app.js header

This commit is contained in:
2026-05-19 14:51:13 +10:00
parent 597c5fd883
commit d31abb68c8
+76 -394
View File
@@ -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 = '<p class="loading-text">No albums found</p>';
return;
}
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];
var thumbUrl = a.albumThumbnailAssetId
? '/api/assets/' + a.albumThumbnailAssetId + '/thumbnail?size=thumbnail'
: '';
var thumbUrl = a.albumThumbnailAssetId ? '/api/assets/' + a.albumThumbnailAssetId + '/thumbnail?size=thumbnail' : '';
html += '<div class="album-item" data-id="' + a.id + '" onclick="selectAlbum(\'' + a.id + '\', this)">';
if (thumbUrl) {
html += '<img class="album-thumb" src="' + thumbUrl + '" alt="" loading="lazy">';
} else {
html += '<div class="album-thumb" style="display:flex;align-items:center;justify-content:center;font-size:1.2rem;">📁</div>';
}
html += '<div class="album-info">';
html += '<div class="album-name">' + escapeHtml(a.albumName) + '</div>';
html += '<div class="album-count">' + a.assetCount + ' photos</div>';
html += '</div></div>';
html += thumbUrl ? '<img class="album-thumb" src="' + thumbUrl + '" 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 + ' photos</div></div></div>';
}
$albumsList.innerHTML = html;
} catch (err) {
$albumsList.innerHTML = '<p class="loading-text">Failed to load albums</p>';
}
} catch (err) { $albumsList.innerHTML = '<p class="loading-text">Failed to load albums</p>'; }
}
// --- 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 = '<span class="spinner"></span> 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();
})();