Slideshow: a site whose path is a local folder (/home/kiosk/photos/, or any existing directory) plays its images full-screen instead of showing Chromium's directory listing. kiosk-app/slideshow.js maps the folder URL to slideshow.html (resolveSiteUrl - used where main.js loads a view and where attachView re-checks the configured URL) and pushes the image list into the page from the main process (the page has no fs access), re-read every minute so added/removed images show up without a restart. Natural file-name order, crossfade, letterboxed by default; optional slideshow.json in the folder: interval, shuffle, fit, transition, recursive. provision_install_app already copies kiosk-app/*.js/*.html, so new installs and Advanced -> Upgrade pick it up. Electron: ^42.0.0 -> ^44.0.0. Electron supports the latest three majors (44/43/42), so 42 drops out when 45 ships. Checked the 43/44 breaking changes against kiosk-app: nothing it uses changed (no clipboard, safeStorage, net.request, utilityProcess; BrowserView is deprecated but still present through 46). The offline bundle builder now retries Electron's install.js download, which crashes outright if a large download is cut off mid-stream. Tested with the real main.js on Electron 44.5.1 under Xvfb + Openbox: slideshow fills the screen, cycles in order (01, 02, 10) skipping a .txt file, honours slideshow.json, and an image added mid-run joined the rotation after the refresh. Offline bundle rebuilt with Electron 44.5.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
105 lines
3.4 KiB
HTML
105 lines
3.4 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Slideshow</title>
|
|
<!-- Shown for a site that's a local folder - see slideshow.js. The image
|
|
list arrives from main.js via window.kioskSlideshow.update(). -->
|
|
<style>
|
|
html, body { margin: 0; height: 100%; background: #000; overflow: hidden; cursor: none; }
|
|
.slide {
|
|
position: absolute; inset: 0; width: 100%; height: 100%;
|
|
object-fit: contain; opacity: 0; transition-property: opacity;
|
|
}
|
|
.slide.show { opacity: 1; }
|
|
#msg {
|
|
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
|
|
color: #888; font: 24px sans-serif; text-align: center; padding: 2em; box-sizing: border-box;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<img class="slide" id="a" alt="">
|
|
<img class="slide" id="b" alt="">
|
|
<div id="msg">Loading slideshow…</div>
|
|
<script>
|
|
(() => {
|
|
const layers = [document.getElementById('a'), document.getElementById('b')];
|
|
const msg = document.getElementById('msg');
|
|
let front = 0;
|
|
let order = [];
|
|
let pos = -1;
|
|
let timer = null;
|
|
let state = null;
|
|
|
|
function shuffled(list) {
|
|
const a = list.slice();
|
|
for (let i = a.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[a[i], a[j]] = [a[j], a[i]];
|
|
}
|
|
return a;
|
|
}
|
|
|
|
function showMessage(text) {
|
|
msg.textContent = text;
|
|
msg.style.display = 'flex';
|
|
layers.forEach((l) => { l.classList.remove('show'); l.removeAttribute('src'); });
|
|
}
|
|
|
|
// Load the next image into the hidden layer, then crossfade to it.
|
|
// Unreadable/corrupt images are skipped rather than stalling the show.
|
|
function next(attempts = 0) {
|
|
clearTimeout(timer);
|
|
if (!order.length) return;
|
|
if (attempts >= order.length) { showMessage('None of the images in this folder could be shown.'); return; }
|
|
pos = (pos + 1) % order.length;
|
|
if (pos === 0 && state.options.shuffle && order.length > 1) order = shuffled(state.images);
|
|
const back = layers[1 - front];
|
|
back.onload = () => {
|
|
msg.style.display = 'none';
|
|
back.classList.add('show');
|
|
layers[front].classList.remove('show');
|
|
front = 1 - front;
|
|
if (order.length > 1) timer = setTimeout(next, state.options.interval * 1000);
|
|
};
|
|
back.onerror = () => next(attempts + 1);
|
|
back.src = order[pos];
|
|
}
|
|
|
|
window.kioskSlideshow = {
|
|
update(s) {
|
|
const first = !state;
|
|
const sameImages = state && JSON.stringify(state.images) === JSON.stringify(s.images);
|
|
const current = order[pos];
|
|
state = s;
|
|
layers.forEach((l) => {
|
|
l.style.objectFit = s.options.fit;
|
|
l.style.transitionDuration = s.options.transition + 's';
|
|
});
|
|
if (!s.images.length) {
|
|
order = [];
|
|
clearTimeout(timer);
|
|
showMessage(s.exists ? `No images in ${s.dir}` : `Folder not found: ${s.dir}`);
|
|
return;
|
|
}
|
|
if (sameImages && !first) return;
|
|
// Folder changed: keep showing the current picture if it's still
|
|
// there and carry on from it, otherwise restart from the top.
|
|
order = s.options.shuffle ? shuffled(s.images) : s.images.slice();
|
|
const keep = current ? order.indexOf(current) : -1;
|
|
if (keep >= 0 && !first) {
|
|
pos = keep;
|
|
clearTimeout(timer);
|
|
if (order.length > 1) timer = setTimeout(next, s.options.interval * 1000);
|
|
} else {
|
|
pos = -1;
|
|
next();
|
|
}
|
|
},
|
|
};
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|