feat: URL params (?album=, ?person=, ?favorites, ?random), periodic refresh, person support

This commit is contained in:
2026-05-19 15:59:43 +10:00
parent be6f0fc30f
commit 4feaaf30d0
+103 -157
View File
@@ -1,56 +1,44 @@
// === Frambe - Frontend Application === // === Frambe - Frontend Application ===
(function () { (function () {
'use strict'; 'use strict';
var config = {}, assets = [], currentIndex = -1, activeLayer = 'a', slideshowTimer = null;
var overlayVisible = true, overlayTimeout = null, selectedSource = null, selectedAlbumId = null;
var selectedPersonId = null, isRunning = false, refreshTimer = null, urlDriven = false;
// --- State --- var $setupScreen = document.getElementById('setup-screen'), $slideshowScreen = document.getElementById('slideshow-screen');
var config = {}; var $connectionStatus = document.getElementById('connection-status'), $setupContent = document.getElementById('setup-content');
var assets = []; var $setupError = document.getElementById('setup-error'), $errorDetail = document.getElementById('error-detail');
var currentIndex = -1; var $albumsList = document.getElementById('albums-list'), $btnStart = document.getElementById('btn-start');
var activeLayer = 'a'; var $layerA = document.getElementById('photo-layer-a'), $layerB = document.getElementById('photo-layer-b');
var slideshowTimer = null; var $bgBlur = document.getElementById('bg-blur'), $clock = document.getElementById('clock');
var progressTimer = null; var $dateDisplay = document.getElementById('date-display'), $exifInfo = document.getElementById('exif-info');
var progressStart = 0; var $progressFill = document.getElementById('progress-fill'), $overlay = document.getElementById('overlay');
var overlayVisible = true; var $btnSettings = document.getElementById('btn-settings'), $progressBar = document.getElementById('progress-bar');
var overlayTimeout = null;
var selectedSource = null;
var selectedAlbumId = null;
var isRunning = false;
var preloadedImages = {};
// --- DOM Elements --- function getUrlParams() {
var $setupScreen = document.getElementById('setup-screen'); var p = {}, s = window.location.search.substring(1); if (!s) return p;
var $slideshowScreen = document.getElementById('slideshow-screen'); var pairs = s.split('&');
var $connectionStatus = document.getElementById('connection-status'); for (var i = 0; i < pairs.length; i++) { var kv = pairs[i].split('='); p[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1] || ''); }
var $setupContent = document.getElementById('setup-content'); return p;
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');
async function init() { async function init() {
document.body.classList.add('setup-mode'); document.body.classList.add('setup-mode');
try { try {
var configRes = await fetch('/api/config'); config = await (await fetch('/api/config')).json();
config = await configRes.json();
if (!config.connected) { showError('API key not configured. Set IMMICH_API_KEY in your environment.'); return; } if (!config.connected) { showError('API key not configured. Set IMMICH_API_KEY in your environment.'); return; }
var serverRes = await fetch('/api/server-info'); var si = await (await fetch('/api/server-info')).json();
var serverInfo = await serverRes.json(); if (!si.ok) { showError('Cannot reach Immich server: ' + si.error); return; }
if (!serverInfo.ok) { showError('Cannot reach Immich server: ' + serverInfo.error); return; } $connectionStatus.textContent = 'Connected to Immich v' + si.version.major + '.' + si.version.minor + '.' + si.version.patch;
$connectionStatus.textContent = 'Connected to Immich v' + serverInfo.version.major + '.' + serverInfo.version.minor + '.' + serverInfo.version.patch;
$connectionStatus.classList.add('connected'); $connectionStatus.classList.add('connected');
await loadAlbums(); var params = getUrlParams();
if (params.album) { urlDriven = true; selectedSource = 'album'; selectedAlbumId = params.album; $btnStart.disabled = false; startSlideshow(); return; }
if (params.person) { urlDriven = true; selectedSource = 'person'; selectedPersonId = params.person; $btnStart.disabled = false; startSlideshow(); return; }
if (params.favorites === '' || params.favorites === 'true' || params.favorites === '1') { urlDriven = true; selectedSource = 'favorites'; $btnStart.disabled = false; startSlideshow(); return; }
if (params.random === '' || params.random === 'true' || params.random === '1') { urlDriven = true; selectedSource = 'random'; $btnStart.disabled = false; startSlideshow(); return; }
if (config.albumId) { selectedSource = 'album'; selectedAlbumId = config.albumId; $btnStart.disabled = false; startSlideshow(); return; } if (config.albumId) { selectedSource = 'album'; selectedAlbumId = config.albumId; $btnStart.disabled = false; startSlideshow(); return; }
if (config.showFavoritesOnly) { selectedSource = 'favorites'; $btnStart.disabled = false; startSlideshow(); return; } if (config.showFavoritesOnly) { selectedSource = 'favorites'; $btnStart.disabled = false; startSlideshow(); return; }
await loadAlbums();
} catch (err) { showError('Failed to initialize: ' + err.message); } } catch (err) { showError('Failed to initialize: ' + err.message); }
} }
@@ -58,180 +46,138 @@
async function loadAlbums() { async function loadAlbums() {
try { try {
var res = await fetch('/api/albums'); var albums = await (await fetch('/api/albums')).json();
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 = ''; var html = '';
for (var i = 0; i < albums.length; i++) { for (var i = 0; i < albums.length; i++) {
var a = albums[i]; var a = albums[i], thu = 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)">'; html += '<div class="album-item" data-id="' + a.id + '" onclick="selectAlbum(\'' + a.id + '\', this)">';
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 += 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 + ' photos</div></div></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; $albumsList.innerHTML = html;
} catch (err) { $albumsList.innerHTML = '<p class="loading-text">Failed to load albums</p>'; } } catch (e) { $albumsList.innerHTML = '<p class="loading-text">Failed to load albums</p>'; }
} }
window.selectSource = function (source) { window.selectSource = function (src) {
selectedSource = source; selectedAlbumId = null; selectedSource = src; selectedAlbumId = null; selectedPersonId = null;
document.getElementById('btn-all-photos').classList.toggle('selected', source === 'random'); document.getElementById('btn-all-photos').classList.toggle('selected', src === 'random');
document.getElementById('btn-favorites').classList.toggle('selected', source === 'favorites'); document.getElementById('btn-favorites').classList.toggle('selected', src === 'favorites');
var albumItems = document.querySelectorAll('.album-item'); var items = document.querySelectorAll('.album-item');
for (var i = 0; i < albumItems.length; i++) albumItems[i].classList.remove('selected'); for (var i = 0; i < items.length; i++) items[i].classList.remove('selected');
$btnStart.disabled = false; $btnStart.disabled = false;
}; };
window.selectAlbum = function (id, el) {
window.selectAlbum = function (albumId, el) { selectedSource = 'album'; selectedAlbumId = id; selectedPersonId = null;
selectedSource = 'album'; selectedAlbumId = albumId;
document.getElementById('btn-all-photos').classList.remove('selected'); document.getElementById('btn-all-photos').classList.remove('selected');
document.getElementById('btn-favorites').classList.remove('selected'); document.getElementById('btn-favorites').classList.remove('selected');
var albumItems = document.querySelectorAll('.album-item'); var items = document.querySelectorAll('.album-item');
for (var i = 0; i < albumItems.length; i++) albumItems[i].classList.remove('selected'); for (var i = 0; i < items.length; i++) items[i].classList.remove('selected');
el.classList.add('selected'); el.classList.add('selected'); $btnStart.disabled = false;
$btnStart.disabled = false;
}; };
async function loadAssets() { async function loadAssets() {
var res; if (selectedSource === 'album' && selectedAlbumId) { var al = await (await fetch('/api/albums/' + selectedAlbumId)).json(); assets = al.assets || []; }
if (selectedSource === 'album' && selectedAlbumId) { res = await fetch('/api/albums/' + selectedAlbumId); var album = await res.json(); assets = album.assets || []; } else if (selectedSource === 'person' && selectedPersonId) { assets = await (await fetch('/api/people/' + selectedPersonId)).json(); }
else if (selectedSource === 'favorites') { res = await fetch('/api/assets/favorites'); assets = await res.json(); } else if (selectedSource === 'favorites') { assets = await (await fetch('/api/assets/favorites')).json(); }
else { res = await fetch('/api/assets/random?count=100'); assets = await res.json(); } else { assets = await (await fetch('/api/assets/random?count=100')).json(); }
if (config.shuffle) shuffleArray(assets); if (config.shuffle) shuffleArray(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: added ' + added + ' new photo(s)');
} catch (e) { console.warn('Frambe: refresh failed', e.message); }
}, (config.refreshInterval || 300) * 1000);
}
window.startSlideshow = async function () { window.startSlideshow = async function () {
if (!selectedSource) return; if (!selectedSource) return;
$btnStart.disabled = true; $btnStart.disabled = true; $btnStart.innerHTML = '<span class="spinner"></span> Loading…';
$btnStart.innerHTML = '<span class="spinner"></span> Loading…';
try { try {
await loadAssets(); await loadAssets();
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;
var transMs = (config.transitionDuration || 2) * 1000; var t = (config.transitionDuration || 2) * 1000;
$layerA.style.transition = 'opacity ' + transMs + 'ms ease'; $layerA.style.transition = 'opacity ' + t + 'ms ease'; $layerB.style.transition = 'opacity ' + t + 'ms ease';
$layerB.style.transition = 'opacity ' + transMs + 'ms ease'; $bgBlur.style.transition = 'opacity ' + (t * 0.75) + '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.showClock) $clock.style.display = 'none'; if (!config.showExif) $exifInfo.style.display = 'none'; if (!config.showProgress) $progressBar.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'; if (!config.backgroundBlur) $bgBlur.style.display = 'none';
updateClock(); setInterval(updateClock, 1000); updateClock(); setInterval(updateClock, 1000);
currentIndex = -1; showNextPhoto(); scheduleOverlayHide(); currentIndex = -1; showNextPhoto(); scheduleOverlayHide(); startRefreshTimer();
} catch (err) { $btnStart.textContent = 'Error: ' + err.message; setTimeout(function () { $btnStart.textContent = '▶ Start Slideshow'; $btnStart.disabled = false; }, 3000); } } catch (err) { $btnStart.textContent = 'Error: ' + err.message; setTimeout(function () { $btnStart.textContent = '▶ Start Slideshow'; $btnStart.disabled = false; }, 3000); }
}; };
window.exitSlideshow = function () { window.exitSlideshow = function () {
isRunning = false; clearTimeout(slideshowTimer); clearInterval(progressTimer); if (urlDriven) { window.location.href = window.location.pathname; return; }
$slideshowScreen.style.display = 'none'; $setupScreen.style.display = 'flex'; isRunning = false; clearTimeout(slideshowTimer); if (refreshTimer) clearInterval(refreshTimer);
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;
$layerA.style.backgroundImage = ''; $layerB.style.backgroundImage = ''; $bgBlur.style.backgroundImage = ''; $layerA.style.backgroundImage = ''; $layerB.style.backgroundImage = ''; $bgBlur.style.backgroundImage = '';
$layerA.classList.add('active'); $layerB.classList.remove('active'); $bgBlur.classList.remove('visible'); $layerA.classList.add('active'); $layerB.classList.remove('active'); $bgBlur.classList.remove('visible'); activeLayer = 'a';
activeLayer = 'a';
}; };
function showNextPhoto() { currentIndex++; if (currentIndex >= assets.length) { if (config.shuffle) shuffleArray(assets); currentIndex = 0; } 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 showPrevPhoto() { currentIndex--; if (currentIndex < 0) currentIndex = assets.length - 1; showPhoto(currentIndex); }
function showPhoto(idx) {
function showPhoto(index) { if (!assets[idx]) return; clearTimeout(slideshowTimer);
if (!assets[index]) return; var a = assets[idx], url = '/api/assets/' + a.id + '/thumbnail?size=preview';
clearTimeout(slideshowTimer); clearInterval(progressTimer); var img = new Image(); img.onload = function () { displayImage(url, a); }; img.onerror = function () { setTimeout(showNextPhoto, 500); }; img.src = url;
var asset = assets[index]; preloadNext(idx + 1);
var imageUrl = '/api/assets/' + asset.id + '/thumbnail?size=preview';
var img = new Image();
img.onload = function () { displayImage(imageUrl, asset); };
img.onerror = function () { setTimeout(showNextPhoto, 500); };
img.src = imageUrl;
preloadNext(index + 1);
} }
function displayImage(url, asset) { function displayImage(url, asset) {
var fitStyle = config.imageFit || 'contain'; var fit = config.imageFit || 'contain', inc, out;
var incomingLayer, outgoingLayer; if (activeLayer === 'a') { inc = $layerB; out = $layerA; activeLayer = 'b'; } else { inc = $layerA; out = $layerB; activeLayer = 'a'; }
if (activeLayer === 'a') { incomingLayer = $layerB; outgoingLayer = $layerA; activeLayer = 'b'; } inc.style.backgroundImage = 'url(' + url + ')'; inc.style.backgroundSize = fit;
else { incomingLayer = $layerA; outgoingLayer = $layerB; activeLayer = 'a'; } inc.classList.add('active'); out.classList.remove('active');
incomingLayer.style.backgroundImage = 'url(' + url + ')';
incomingLayer.style.backgroundSize = fitStyle;
incomingLayer.classList.add('active'); outgoingLayer.classList.remove('active');
if (config.backgroundBlur) { $bgBlur.style.backgroundImage = 'url(' + url + ')'; $bgBlur.classList.add('visible'); } if (config.backgroundBlur) { $bgBlur.style.backgroundImage = 'url(' + url + ')'; $bgBlur.classList.add('visible'); }
updateExifInfo(asset); startProgress(); updateExifInfo(asset); startProgress();
var interval = (config.slideshowInterval || 30) * 1000; slideshowTimer = setTimeout(showNextPhoto, (config.slideshowInterval || 30) * 1000);
slideshowTimer = setTimeout(showNextPhoto, interval);
} }
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 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 updateExifInfo(a) {
if (!config.showExif || !a.exifInfo) { $exifInfo.textContent = ''; return; }
function updateExifInfo(asset) { var p = [], e = a.exifInfo, loc = [e.city, e.state, e.country].filter(Boolean).join(', ');
if (!config.showExif || !asset.exifInfo) { $exifInfo.textContent = ''; return; } if (loc) p.push('📍 ' + loc);
var parts = [], exif = asset.exifInfo; if (e.dateTimeOriginal) p.push(formatDate(new Date(e.dateTimeOriginal)));
var location = [exif.city, exif.state, exif.country].filter(Boolean).join(', '); else if (a.fileCreatedAt) p.push(formatDate(new Date(a.fileCreatedAt)));
if (location) parts.push('📍 ' + location); if (e.make || e.model) p.push('📷 ' + [e.make, e.model].filter(Boolean).join(' '));
if (exif.dateTimeOriginal) { parts.push(formatDate(new Date(exif.dateTimeOriginal))); } $exifInfo.textContent = p.join(' • ');
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(' • ');
} }
function startProgress() { function startProgress() {
if (!config.showProgress) return; if (!config.showProgress) return;
$progressFill.style.transition = 'none'; $progressFill.style.width = '0%'; $progressFill.style.transition = 'none'; $progressFill.style.width = '0%'; $progressFill.offsetWidth;
progressStart = Date.now(); var duration = (config.slideshowInterval || 30) * 1000; $progressFill.style.transition = 'width ' + ((config.slideshowInterval || 30) * 1000) + 'ms linear'; $progressFill.style.width = '100%';
$progressFill.offsetWidth;
$progressFill.style.transition = 'width ' + duration + 'ms linear'; $progressFill.style.width = '100%';
} }
function updateClock() { function updateClock() {
var now = new Date(); var n = new Date();
if (config.showClock) { $clock.textContent = padZero(now.getHours()) + ':' + padZero(now.getMinutes()); } if (config.showClock) $clock.textContent = padZero(n.getHours()) + ':' + padZero(n.getMinutes());
if (config.showDate) { $dateDisplay.textContent = now.toLocaleDateString(undefined, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }); } 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'); } };
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); } 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.nextPhoto = function () { showNextPhoto(); if (overlayVisible) scheduleOverlayHide(); };
window.prevPhoto = function () { showPrevPhoto(); 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; } });
document.addEventListener('keydown', function (e) { 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 (!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 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 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 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 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) {} } 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(); }); 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){} }
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(); init(); requestWakeLock(); preventSleep();
})(); })();