Folder image slideshow sites; Electron 42 -> 44
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
This commit is contained in:
@@ -197,6 +197,14 @@ The kiosk machine still needs a working internet connection (ethernet, or WiFi c
|
|||||||
instead of a web address and it's shown from the kiosk's own disk:
|
instead of a web address and it's shown from the kiosk's own disk:
|
||||||
HTML pages, PDFs (built-in viewer), and images. Works offline. The file
|
HTML pages, PDFs (built-in viewer), and images. Works offline. The file
|
||||||
must be readable by the `kiosk` user.
|
must be readable by the `kiosk` user.
|
||||||
|
- **Image slideshow** - Enter a folder instead (e.g. `/home/kiosk/photos/`)
|
||||||
|
and its images (jpg, png, gif, webp, bmp, svg, avif) play full-screen in
|
||||||
|
file-name order with a crossfade, 10 seconds each. Images added to or
|
||||||
|
removed from the folder are picked up within a minute, no restart
|
||||||
|
needed. Optional `slideshow.json` in the folder:
|
||||||
|
`{"interval": 10, "shuffle": false, "fit": "contain", "transition": 1, "recursive": false}`
|
||||||
|
(`fit`: `contain` shows the whole image, `cover` fills the screen and
|
||||||
|
crops; `recursive` includes subfolders).
|
||||||
|
|
||||||
### Touch Controls
|
### Touch Controls
|
||||||
- **2-finger horizontal swipe** - Switch between sites
|
- **2-finger horizontal swipe** - Switch between sites
|
||||||
@@ -396,7 +404,7 @@ Both can be used at the same time — they serve different purposes:
|
|||||||
## What This Script Installs
|
## What This Script Installs
|
||||||
|
|
||||||
### Core Components
|
### Core Components
|
||||||
- **Electron** v42.x (Chromium-based app framework)
|
- **Electron** v44.x (Chromium-based app framework)
|
||||||
- **Node.js** v20.x with npm
|
- **Node.js** v20.x with npm
|
||||||
- **Openbox** - Lightweight window manager
|
- **Openbox** - Lightweight window manager
|
||||||
- **LightDM** - Display manager with autologin
|
- **LightDM** - Display manager with autologin
|
||||||
@@ -1453,9 +1461,16 @@ full migration pass.
|
|||||||
|
|
||||||
## Project Status & Future Plans
|
## Project Status & Future Plans
|
||||||
|
|
||||||
**Current Version:** 2.17.0
|
**Current Version:** 2.18.0
|
||||||
|
|
||||||
**Recent Updates (v2.17.0):**
|
**Recent Updates (v2.18.0):**
|
||||||
|
- **All-in-one install ISO** (`iso/build-iso.sh`, or Actions → Build kiosk ISO): Ubuntu Server 26.04 plus the kiosk in one USB stick — the Ubuntu install, then `install.sh` automatically on first boot. Interactive, unattended, or fully automatic. See "All-in-One Install ISO" above.
|
||||||
|
- **Offline installs** (`--offline`): the ISO carries every package (including CUPS printing and all printer drivers), Node.js 22, and prebuilt Electron/Web UI modules, so a kiosk installs with no internet. With internet it still installs current versions online. Addons → CUPS Printing (menu or Web UI) installs from the bundle by itself when offline.
|
||||||
|
- **Local files as sites:** an absolute path (`/home/kiosk/docs/menu.pdf`) shows a local page, PDF or image; a folder (`/home/kiosk/photos/`) plays as a full-screen **image slideshow** that picks up added/removed images within a minute. Works in both the Sites menu and the Web UI.
|
||||||
|
- **Electron upgrade:** v42.x → v44.x (42 leaves Electron's support window when 45 ships). Existing kiosks move to it via Advanced → Upgrade.
|
||||||
|
- **Fix:** the CUPS menu could report an installed CUPS as "not installed" (`dpkg -l | grep -q` failing under `set -o pipefail`).
|
||||||
|
|
||||||
|
**Previous (v2.17.0):**
|
||||||
- **Web UI now installs by default** during first-time provisioning (fixed port 8090, no prompt) instead of being opt-in — the Addons menu entry still works standalone for reconfiguring the port or reinstalling it on a kiosk provisioned before this change.
|
- **Web UI now installs by default** during first-time provisioning (fixed port 8090, no prompt) instead of being opt-in — the Addons menu entry still works standalone for reconfiguring the port or reinstalling it on a kiosk provisioned before this change.
|
||||||
- **The web UI can now install/reconfigure CUPS Printing, LMS Server, Squeezelite Player, and Asterisk Intercom, and check for updates** — the same four addons plus Update named directly. Every one of these is the exact same interactive `action_*` function the terminal menu already uses (no prompt/mutation refactor of any addon file), driven by piping the right answers on stdin — the same technique this project's own bash tests already use to drive these functions.
|
- **The web UI can now install/reconfigure CUPS Printing, LMS Server, Squeezelite Player, and Asterisk Intercom, and check for updates** — the same four addons plus Update named directly. Every one of these is the exact same interactive `action_*` function the terminal menu already uses (no prompt/mutation refactor of any addon file), driven by piping the right answers on stdin — the same technique this project's own bash tests already use to drive these functions.
|
||||||
- **Privilege model:** the web service itself still runs as `$KIOSK_USER` with zero ambient `sudo`. A new narrow, allow-listed root helper is the only way it ever gains privilege — reachable only via a single-path passwordless sudo rule (generated and validated with `visudo -c -f` before being installed), and it re-checks its own fixed action allow-list before dispatching anything. Chosen over running the whole service as root after asking directly: since this repo has no login of its own by design, a request that reaches the web UI with no reverse proxy in front is effectively unauthenticated, so the allow-list bounds what that can actually do to five vetted actions, never a root shell.
|
- **Privilege model:** the web service itself still runs as `$KIOSK_USER` with zero ambient `sudo`. A new narrow, allow-listed root helper is the only way it ever gains privilege — reachable only via a single-path passwordless sudo rule (generated and validated with `visudo -c -f` before being installed), and it re-checks its own fixed action allow-list before dispatching anything. Chosen over running the whole service as root after asking directly: since this repo has no login of its own by design, a request that reaches the web UI with no reverse proxy in front is effectively unauthenticated, so the allow-list bounds what that can actually do to five vetted actions, never a root shell.
|
||||||
|
|||||||
@@ -185,7 +185,14 @@ build_modules() {
|
|||||||
# postinstall since ~v42) - lib/electron.sh runs this same install.js
|
# postinstall since ~v42) - lib/electron.sh runs this same install.js
|
||||||
# on the target when online; offline it has to already be in dist/.
|
# on the target when online; offline it has to already be in dist/.
|
||||||
if [[ -f "$tmp/node_modules/electron/install.js" ]]; then
|
if [[ -f "$tmp/node_modules/electron/install.js" ]]; then
|
||||||
(cd "$tmp" && node node_modules/electron/install.js) || die "Electron binary download failed"
|
# Retried: a large download cut off mid-stream crashes it outright.
|
||||||
|
local try
|
||||||
|
for try in 1 2 3; do
|
||||||
|
(cd "$tmp" && node node_modules/electron/install.js) && break
|
||||||
|
(( try < 3 )) || die "Electron binary download failed (3 attempts)"
|
||||||
|
info "Electron download failed - retrying ($((try + 1))/3)..."
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
tar -czf "$OUT_DIR/npm/${name}-node_modules.tar.gz" --owner=0 --group=0 -C "$tmp" node_modules
|
tar -czf "$OUT_DIR/npm/${name}-node_modules.tar.gz" --owner=0 --group=0 -C "$tmp" node_modules
|
||||||
info "$name: $(du -sh "$tmp/node_modules" | cut -f1) -> $(du -h "$OUT_DIR/npm/${name}-node_modules.tar.gz" | cut -f1) compressed"
|
info "$name: $(du -sh "$tmp/node_modules" | cut -f1) -> $(du -h "$OUT_DIR/npm/${name}-node_modules.tar.gz" | cut -f1) compressed"
|
||||||
|
|||||||
+5
-1
@@ -4,6 +4,8 @@ const fs=require('fs');
|
|||||||
const path=require('path');
|
const path=require('path');
|
||||||
const os=require('os');
|
const os=require('os');
|
||||||
const crypto=require('crypto');
|
const crypto=require('crypto');
|
||||||
|
// Sites that are a local folder show as an image slideshow (slideshow.js).
|
||||||
|
const slideshow=require('./slideshow');
|
||||||
|
|
||||||
// Suppress EPIPE errors (happen when no terminal attached)
|
// Suppress EPIPE errors (happen when no terminal attached)
|
||||||
process.stdout.on('error',(e)=>{if(e.code!=='EPIPE')throw e;});
|
process.stdout.on('error',(e)=>{if(e.code!=='EPIPE')throw e;});
|
||||||
@@ -782,7 +784,7 @@ function attachView(i){
|
|||||||
|
|
||||||
const tabIdx=viewIndexToTabIndex(i);
|
const tabIdx=viewIndexToTabIndex(i);
|
||||||
if(tabIdx>=0&&tabs[tabIdx]){
|
if(tabIdx>=0&&tabs[tabIdx]){
|
||||||
const configuredUrl=tabs[tabIdx].url;
|
const configuredUrl=slideshow.resolveSiteUrl(tabs[tabIdx].url);
|
||||||
const currentUrl=views[i].webContents.getURL();
|
const currentUrl=views[i].webContents.getURL();
|
||||||
|
|
||||||
if(currentUrl&&!currentUrl.startsWith(configuredUrl)){
|
if(currentUrl&&!currentUrl.startsWith(configuredUrl)){
|
||||||
@@ -1334,6 +1336,7 @@ async function createWindow(){
|
|||||||
url=u.toString();
|
url=u.toString();
|
||||||
}catch(e){}
|
}catch(e){}
|
||||||
}
|
}
|
||||||
|
url=slideshow.resolveSiteUrl(url);
|
||||||
|
|
||||||
const initialOrigin=new URL(t.url).origin;
|
const initialOrigin=new URL(t.url).origin;
|
||||||
|
|
||||||
@@ -1367,6 +1370,7 @@ async function createWindow(){
|
|||||||
});
|
});
|
||||||
|
|
||||||
view.webContents.setAudioMuted(false);
|
view.webContents.setAudioMuted(false);
|
||||||
|
slideshow.attach(view.webContents);
|
||||||
view.webContents.loadURL(url);
|
view.webContents.loadURL(url);
|
||||||
|
|
||||||
view.webContents.on('did-finish-load',()=>{
|
view.webContents.on('did-finish-load',()=>{
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"electron": "^42.0.0"
|
"electron": "^44.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<!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>
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
// Folder slideshow support for main.js.
|
||||||
|
//
|
||||||
|
// A site whose URL is a local folder (file:///home/kiosk/photos, entered
|
||||||
|
// in the Sites menu or Web UI as /home/kiosk/photos/) is shown as a
|
||||||
|
// full-screen slideshow of the images in it, instead of Chromium's
|
||||||
|
// directory listing. main.js swaps the folder URL for slideshow.html via
|
||||||
|
// resolveSiteUrl(), then hands the page its image list with attach() -
|
||||||
|
// the page itself has no filesystem access (contextIsolation, no Node).
|
||||||
|
// The folder is re-read every minute, so images added or removed show up
|
||||||
|
// without restarting the kiosk.
|
||||||
|
//
|
||||||
|
// Optional slideshow.json in the folder (all keys optional):
|
||||||
|
// { "interval": 10, seconds per image (default 10, minimum 2)
|
||||||
|
// "shuffle": false, random order instead of by file name
|
||||||
|
// "fit": "contain", "contain" (whole image, letterboxed) or
|
||||||
|
// "cover" (fill the screen, cropped)
|
||||||
|
// "transition": 1, crossfade seconds (0 = instant cut)
|
||||||
|
// "recursive": false } include images in subfolders
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { fileURLToPath, pathToFileURL } = require('url');
|
||||||
|
|
||||||
|
const SLIDESHOW_PAGE = path.join(__dirname, 'slideshow.html');
|
||||||
|
const IMAGE_EXT = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg', '.avif']);
|
||||||
|
const REFRESH_MS = 60000;
|
||||||
|
const DEFAULTS = { interval: 10, shuffle: false, fit: 'contain', transition: 1, recursive: false };
|
||||||
|
|
||||||
|
// The folder path a site URL points at, or null if it isn't a slideshow.
|
||||||
|
// A file:// URL counts when it's an existing directory, or ends in "/"
|
||||||
|
// (so a folder that isn't there yet - e.g. a USB stick not plugged in -
|
||||||
|
// still shows the slideshow's "no images" message, not an error page).
|
||||||
|
function slideshowDir(url) {
|
||||||
|
if (typeof url !== 'string' || !url.startsWith('file://')) return null;
|
||||||
|
let p;
|
||||||
|
try {
|
||||||
|
p = fileURLToPath(url);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (url.endsWith('/')) return p;
|
||||||
|
try {
|
||||||
|
return fs.statSync(p).isDirectory() ? p : null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// What a view should actually load for a configured site URL.
|
||||||
|
function resolveSiteUrl(url) {
|
||||||
|
const dir = slideshowDir(url);
|
||||||
|
if (!dir) return url;
|
||||||
|
const u = pathToFileURL(SLIDESHOW_PAGE);
|
||||||
|
u.searchParams.set('dir', dir);
|
||||||
|
return u.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readOptions(dir) {
|
||||||
|
const opts = { ...DEFAULTS };
|
||||||
|
try {
|
||||||
|
const user = JSON.parse(fs.readFileSync(path.join(dir, 'slideshow.json'), 'utf8'));
|
||||||
|
if (Number(user.interval) > 0) opts.interval = Math.max(2, Number(user.interval));
|
||||||
|
if (typeof user.shuffle === 'boolean') opts.shuffle = user.shuffle;
|
||||||
|
if (user.fit === 'contain' || user.fit === 'cover') opts.fit = user.fit;
|
||||||
|
if (Number(user.transition) >= 0) opts.transition = Math.min(Number(user.transition), opts.interval / 2);
|
||||||
|
if (typeof user.recursive === 'boolean') opts.recursive = user.recursive;
|
||||||
|
} catch (e) {
|
||||||
|
// No (or unreadable) slideshow.json - defaults.
|
||||||
|
}
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function listImages(dir, recursive, depth = 0) {
|
||||||
|
let entries;
|
||||||
|
try {
|
||||||
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const out = [];
|
||||||
|
for (const e of entries) {
|
||||||
|
if (e.name.startsWith('.')) continue;
|
||||||
|
const full = path.join(dir, e.name);
|
||||||
|
if (e.isDirectory()) {
|
||||||
|
if (recursive && depth < 8) out.push(...listImages(full, recursive, depth + 1));
|
||||||
|
} else if (IMAGE_EXT.has(path.extname(e.name).toLowerCase())) {
|
||||||
|
out.push(full);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapshot(dir) {
|
||||||
|
const options = readOptions(dir);
|
||||||
|
const files = listImages(dir, options.recursive)
|
||||||
|
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }));
|
||||||
|
return {
|
||||||
|
dir,
|
||||||
|
exists: fs.existsSync(dir),
|
||||||
|
options,
|
||||||
|
images: files.map((f) => pathToFileURL(f).href),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feed a view's slideshow page its image list now and every REFRESH_MS
|
||||||
|
// while it stays on a slideshow. Call once per view (main.js does, right
|
||||||
|
// after creating it); safe on views that never show a slideshow.
|
||||||
|
function attach(webContents) {
|
||||||
|
let lastSent = '';
|
||||||
|
const send = (force) => {
|
||||||
|
if (webContents.isDestroyed()) return;
|
||||||
|
const current = webContents.getURL();
|
||||||
|
if (!current.startsWith(pathToFileURL(SLIDESHOW_PAGE).href)) return;
|
||||||
|
const dir = new URL(current).searchParams.get('dir');
|
||||||
|
if (!dir) return;
|
||||||
|
const payload = JSON.stringify(snapshot(dir));
|
||||||
|
if (!force && payload === lastSent) return;
|
||||||
|
lastSent = payload;
|
||||||
|
webContents.executeJavaScript(`window.kioskSlideshow&&window.kioskSlideshow.update(${payload})`)
|
||||||
|
.catch(() => {});
|
||||||
|
};
|
||||||
|
webContents.on('did-finish-load', () => send(true));
|
||||||
|
const timer = setInterval(() => send(false), REFRESH_MS);
|
||||||
|
webContents.once('destroyed', () => clearInterval(timer));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { resolveSiteUrl, slideshowDir, attach, snapshot };
|
||||||
@@ -136,6 +136,9 @@ action_add_page() {
|
|||||||
echo " 0 = manual only (swipe/nav menu to reach it)"
|
echo " 0 = manual only (swipe/nav menu to reach it)"
|
||||||
echo " -1 = hidden (PIN-gated, F10 or 3-finger swipe)"
|
echo " -1 = hidden (PIN-gated, F10 or 3-finger swipe)"
|
||||||
echo
|
echo
|
||||||
|
echo "URL: a web address, or a path on this kiosk - a file (page, PDF,"
|
||||||
|
echo "image) like /home/kiosk/docs/menu.pdf, or a folder of images ending"
|
||||||
|
echo "in / like /home/kiosk/photos/ for a slideshow."
|
||||||
|
|
||||||
local raw_url url dur name needs_auth user pass
|
local raw_url url dur name needs_auth user pass
|
||||||
read -r -p "URL: " raw_url
|
read -r -p "URL: " raw_url
|
||||||
|
|||||||
Reference in New Issue
Block a user