←/→ et contrôles à l'écran changent de projet; navigation manuelle = pause de rotation 90 s; Espace = pause franche avec badge. La position survit aux rechargements via sessionStorage (le cycle de 12 projets x 30 s dépassait le refresh de sécurité de 5 min: le mur semblait figé). Fix: l'écoute SSE utilisait onmessage, inerte pour les événements nommés 'gitea' du hub.
113 lines
4.3 KiB
JavaScript
113 lines
4.3 KiB
JavaScript
/**
|
||
* Kanbante — Wallboard: rotation, manual navigation, clock, live.
|
||
*
|
||
* One slide per active project, rotating every 30 s. The viewer can
|
||
* take over at any time: ←/→ keys (or the on-screen ❮ ❯ buttons and
|
||
* dots) switch projects immediately and pause the rotation for
|
||
* 90 s; Space toggles a hard pause. The current position survives
|
||
* the live/safety reloads via sessionStorage, so the wall no longer
|
||
* appears stuck on the first project after every refresh.
|
||
*/
|
||
(function () {
|
||
var slides = Array.from(document.querySelectorAll('.wall-slide'));
|
||
var ROTATE_MS = 30000;
|
||
var MANUAL_PAUSE_MS = 90000;
|
||
var STORAGE_KEY = 'kanbante-wall-index';
|
||
|
||
var current = 0;
|
||
var paused = false;
|
||
var pausedUntil = 0;
|
||
var pausedBadge = document.getElementById('wall-paused');
|
||
var dotsBox = document.getElementById('wall-dots');
|
||
|
||
function updateBadge() {
|
||
if (pausedBadge) {
|
||
pausedBadge.hidden = !(paused || Date.now() < pausedUntil);
|
||
}
|
||
}
|
||
|
||
function updateDots() {
|
||
if (!dotsBox) return;
|
||
Array.from(dotsBox.children).forEach(function (dot, i) {
|
||
dot.classList.toggle('on', i === current);
|
||
});
|
||
}
|
||
|
||
function show(index, manual) {
|
||
if (!slides.length) return;
|
||
slides[current].classList.remove('active');
|
||
current = ((index % slides.length) + slides.length) % slides.length;
|
||
slides[current].classList.add('active');
|
||
try { sessionStorage.setItem(STORAGE_KEY, String(current)); } catch (e) { /* private mode */ }
|
||
updateDots();
|
||
if (manual) {
|
||
pausedUntil = Date.now() + MANUAL_PAUSE_MS;
|
||
updateBadge();
|
||
}
|
||
}
|
||
|
||
// Restore the position kept across reloads.
|
||
if (slides.length > 1) {
|
||
var saved = 0;
|
||
try { saved = parseInt(sessionStorage.getItem(STORAGE_KEY) || '0', 10) || 0; } catch (e) { saved = 0; }
|
||
if (saved) show(saved, false);
|
||
|
||
// Dots (one per project, clickable).
|
||
if (dotsBox) {
|
||
slides.forEach(function (_slide, i) {
|
||
var dot = document.createElement('button');
|
||
dot.type = 'button';
|
||
dot.className = 'wall-dot';
|
||
dot.title = 'Projet ' + (i + 1);
|
||
dot.setAttribute('aria-label', 'Afficher le projet ' + (i + 1));
|
||
dot.addEventListener('click', function () { show(i, true); });
|
||
dotsBox.appendChild(dot);
|
||
});
|
||
updateDots();
|
||
}
|
||
var prev = document.getElementById('wall-prev');
|
||
var next = document.getElementById('wall-next');
|
||
if (prev) prev.addEventListener('click', function () { show(current - 1, true); });
|
||
if (next) next.addEventListener('click', function () { show(current + 1, true); });
|
||
|
||
document.addEventListener('keydown', function (e) {
|
||
if (e.key === 'ArrowRight') { e.preventDefault(); show(current + 1, true); }
|
||
else if (e.key === 'ArrowLeft') { e.preventDefault(); show(current - 1, true); }
|
||
else if (e.key === ' ') {
|
||
e.preventDefault();
|
||
paused = !paused;
|
||
if (!paused) pausedUntil = 0;
|
||
updateBadge();
|
||
}
|
||
});
|
||
|
||
setInterval(function () {
|
||
if (!paused && Date.now() >= pausedUntil) show(current + 1, false);
|
||
}, ROTATE_MS);
|
||
setInterval(updateBadge, 5000);
|
||
}
|
||
|
||
function tickClock() {
|
||
var now = new Date();
|
||
var time = now.toLocaleTimeString('fr-CA', { hour: '2-digit', minute: '2-digit' });
|
||
var date = now.toLocaleDateString('fr-CA', {
|
||
weekday: 'long', day: 'numeric', month: 'long',
|
||
});
|
||
document.querySelectorAll('[data-clock]').forEach(function (el) { el.textContent = time; });
|
||
document.querySelectorAll('[data-date]').forEach(function (el) { el.textContent = date; });
|
||
}
|
||
tickClock();
|
||
setInterval(tickClock, 15000);
|
||
|
||
// Live: board activity reloads the wall. The hub emits NAMED
|
||
// "gitea" events — onmessage would never fire (fixed in 0.11.0).
|
||
try {
|
||
var source = new EventSource('/api/stream');
|
||
source.addEventListener('gitea', function () { location.reload(); });
|
||
} catch (err) {
|
||
console.warn('Wallboard live stream unavailable:', err);
|
||
}
|
||
// Safety net.
|
||
setTimeout(function () { location.reload(); }, 5 * 60 * 1000);
|
||
})();
|