Skip to content

Commit 5398cfc

Browse files
committed
Add timeout option
1 parent df452d5 commit 5398cfc

3 files changed

Lines changed: 122 additions & 2 deletions

File tree

Kiosk App/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ BrowserSelector is a fullscreen Electron-based kiosk experience that showcases a
2828
- `config.titleCardDurationMs` controls how long the title card stays visible (defaults to 3000 ms).
2929
- `config.backgroundColor` sets the fallback color behind the title card (`#080808` by default).
3030
- `config.backgroundImagePath` can point to a local file (absolute or relative to `projects.json`) or a remote URL that will fill the viewport behind the card. Relative paths are resolved against the directory containing `projects.json`.
31+
- `config.idleShuffleTimeoutMs` sets how long the kiosk waits (defaults to 60000 ms) before automatically loading a random project when there is no interaction.
3132
- When packaged, the app searches for `projects.json` next to the executable, inside the `.app/Contents` directory, alongside the `.app` bundle itself, and then in `~/Desktop` and `~/Downloads`.
3233
- Serial devices are scanned every 5 seconds; any device path containing `usbmodem` or `usb.modem` is considered a candidate.
3334

Kiosk App/main.js

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ const PROJECTS_FILE_NAME = 'projects.json';
1010
const DEFAULT_CONFIG = {
1111
titleCardDurationMs: 3000,
1212
backgroundColor: '#080808',
13-
backgroundImagePath: ''
13+
backgroundImagePath: '',
14+
idleShuffleTimeoutMs: 60000
1415
};
1516
const SERIAL_SCAN_INTERVAL_MS = 5000;
1617

@@ -32,6 +33,8 @@ let serialScanInProgress = false;
3233
let isTitleCardVisible = false;
3334
let titleCardLoadingPromise = null;
3435
let missingProjectsAlertShown = false;
36+
let idleTimerId = null;
37+
let mouseHoldIntervalId = null;
3538

3639
function resolveBackgroundImageParam(imagePath) {
3740
if (!imagePath || typeof imagePath !== 'string') {
@@ -122,6 +125,62 @@ function getTitleCardDurationMs() {
122125
return DEFAULT_CONFIG.titleCardDurationMs;
123126
}
124127

128+
function getIdleShuffleTimeoutMs() {
129+
const configured = config.idleShuffleTimeoutMs;
130+
if (configured === null || configured === false) return 0;
131+
const timeout = Number(configured);
132+
if (Number.isFinite(timeout) && timeout > 0) {
133+
return timeout;
134+
}
135+
if (timeout === 0) return 0;
136+
return DEFAULT_CONFIG.idleShuffleTimeoutMs;
137+
}
138+
139+
function resetIdleTimer() {
140+
if (idleTimerId) {
141+
clearTimeout(idleTimerId);
142+
idleTimerId = null;
143+
}
144+
145+
const timeout = getIdleShuffleTimeoutMs();
146+
if (!Number.isFinite(timeout) || timeout <= 0) {
147+
return;
148+
}
149+
150+
idleTimerId = setTimeout(() => {
151+
idleTimerId = null;
152+
handleIdleTimeout();
153+
}, timeout);
154+
}
155+
156+
function startMouseHoldInterval() {
157+
if (mouseHoldIntervalId) return;
158+
mouseHoldIntervalId = setInterval(() => resetIdleTimer(), 250);
159+
}
160+
161+
function stopMouseHoldInterval() {
162+
if (!mouseHoldIntervalId) return;
163+
clearInterval(mouseHoldIntervalId);
164+
mouseHoldIntervalId = null;
165+
}
166+
167+
function selectRandomProjectIndex(excludeIndex) {
168+
if (!projects.length) return 0;
169+
if (projects.length === 1) return 0;
170+
171+
let idx = Math.floor(Math.random() * projects.length);
172+
if (idx === excludeIndex) {
173+
idx = (idx + 1 + Math.floor(Math.random() * (projects.length - 1))) % projects.length;
174+
}
175+
return idx;
176+
}
177+
178+
function handleIdleTimeout() {
179+
if (!projects.length) return;
180+
const nextIndex = selectRandomProjectIndex(currentProjectIndex);
181+
loadProject(nextIndex);
182+
}
183+
125184
function showProjectAfterTitleCard(project) {
126185
if (!mainWindow) {
127186
throw new Error('Main window is not available to display the project.');
@@ -164,6 +223,7 @@ function loadProject(index) {
164223

165224
currentProjectIndex = index;
166225
showProjectAfterTitleCard(project);
226+
resetIdleTimer();
167227
}
168228

169229
function showNextProject() {
@@ -183,8 +243,10 @@ function handleSerialData(data) {
183243
for (const char of input) {
184244
if (char === '>') {
185245
showNextProject();
246+
resetIdleTimer();
186247
} else if (char === '<') {
187248
showPreviousProject();
249+
resetIdleTimer();
188250
}
189251
}
190252
}
@@ -295,10 +357,26 @@ function createWindow() {
295357
if (titleCardTimeoutId) clearTimeout(titleCardTimeoutId);
296358
titleCardTimeoutId = null;
297359
mainWindow = null;
360+
stopMouseHoldInterval();
298361
});
299362

300363
win.webContents.on('before-input-event', (_event, input) => {
364+
if (input.type === 'mouseDown') {
365+
resetIdleTimer();
366+
startMouseHoldInterval();
367+
return;
368+
}
369+
if (input.type === 'mouseUp') {
370+
resetIdleTimer();
371+
stopMouseHoldInterval();
372+
return;
373+
}
374+
if (input.type === 'mouseMove' || input.type === 'mouseWheel') {
375+
resetIdleTimer();
376+
return;
377+
}
301378
if (input.type !== 'keyDown') return;
379+
resetIdleTimer();
302380
if (input.meta && input.key === 'ArrowRight') {
303381
showNextProject();
304382
return;
@@ -314,6 +392,41 @@ function createWindow() {
314392
}
315393
});
316394

395+
const resetEvents = [
396+
'cursor-changed',
397+
'did-start-navigation',
398+
'did-navigate',
399+
'did-frame-finish-load',
400+
'paint',
401+
'scroll-touch-begin',
402+
'scroll-touch-end',
403+
'scroll-begin',
404+
'scroll-end',
405+
'mouse-down',
406+
'mouse-up',
407+
'mouse-enter',
408+
'mouse-leave',
409+
'pointer-lock-change',
410+
'mouse-wheel',
411+
'touch-start',
412+
'touch-end',
413+
'touch-move',
414+
'gesture-begin',
415+
'gesture-end',
416+
'gesture-start',
417+
'gestureupdate',
418+
'gestureend'
419+
];
420+
421+
for (const eventName of resetEvents) {
422+
win.webContents.on(eventName, () => resetIdleTimer());
423+
}
424+
425+
win.webContents.on('input-event', () => resetIdleTimer());
426+
427+
win.on('blur', () => stopMouseHoldInterval());
428+
win.on('leave-full-screen', () => stopMouseHoldInterval());
429+
317430
currentProjectIndex = Math.floor(Math.random() * projects.length);
318431
loadProject(currentProjectIndex);
319432
}
@@ -457,4 +570,9 @@ app.on('window-all-closed', () => {
457570

458571
app.on('before-quit', () => {
459572
stopSerialMonitoring();
573+
if (idleTimerId) {
574+
clearTimeout(idleTimerId);
575+
idleTimerId = null;
576+
}
577+
stopMouseHoldInterval();
460578
});

Kiosk App/projects.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"config": {
33
"titleCardDurationMs": 3000,
44
"backgroundColor": "#080808",
5-
"backgroundImagePath": "./bg.png"
5+
"backgroundImagePath": "./bg.png",
6+
"idleShuffleTimeoutMs": 0
67
},
78
"projects": [
89
{

0 commit comments

Comments
 (0)