From 57d8e82949a307a9c8ba09fdab3abdc62a61e1ee Mon Sep 17 00:00:00 2001 From: librelad Date: Wed, 27 May 2026 16:01:32 +0100 Subject: [PATCH] ui(devmode): roll the easter-egg countdown into one updating toast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking the LibrePortal logo 6→9 times spawned four separate "X clicks away from being a developer" notifications stacked on top of each other — visual noise for a delightful-bonus interaction. Now the easter egg keeps a single reference to its current toast and mutates the `.notification-message` text in place on each subsequent click. When the toast's 10s auto-remove timer expires mid-sequence (slow clicker) the next click opens a fresh one — same fallback for the idle-reset path that clears the count after 3s. `_devToast` now returns the notification element so the easter-egg handler can grab it; previously it returned undefined, fine for the one-shot toasts but no longer enough for the rolling-update pattern. Signed-off-by: librelad --- .../frontend/js/components/topbar.js | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/containers/libreportal/frontend/js/components/topbar.js b/containers/libreportal/frontend/js/components/topbar.js index a3ed49b..8e32942 100755 --- a/containers/libreportal/frontend/js/components/topbar.js +++ b/containers/libreportal/frontend/js/components/topbar.js @@ -89,6 +89,7 @@ class TopbarComponent { let count = 0; let resetTimer = null; + let currentToast = null; // single rolling toast — updated in place document.addEventListener('click', (e) => { const logo = e.target.closest('.libreportal-logo'); @@ -96,7 +97,10 @@ class TopbarComponent { count++; if (resetTimer) clearTimeout(resetTimer); - resetTimer = setTimeout(() => { count = 0; }, RESET_AFTER_MS); + resetTimer = setTimeout(() => { + count = 0; + currentToast = null; // next sequence starts a fresh toast + }, RESET_AFTER_MS); const remaining = TARGET_CLICKS - count; const devOn = (window.systemConfigs?.CFG_DEV_MODE === 'true'); @@ -104,10 +108,22 @@ class TopbarComponent { const noun = devOn ? 'developer mode' : 'a developer'; if (remaining > 0 && count >= TOAST_FROM) { - this._devToast(`You are ${remaining} click${remaining === 1 ? '' : 's'} away from ${verb} ${noun}.`, 'info'); + const msg = `You are ${remaining} click${remaining === 1 ? '' : 's'} away from ${verb} ${noun}.`; + // If the rolling toast is still in the DOM, mutate its message + // text in place instead of stacking another notification. When + // it's been auto-removed (10s lifetime) we open a fresh one. + const msgEl = currentToast && currentToast.parentElement + ? currentToast.querySelector('.notification-message') + : null; + if (msgEl) { + msgEl.innerHTML = msg; + } else { + currentToast = this._devToast(msg, 'info'); + } } else if (remaining === 0) { count = 0; clearTimeout(resetTimer); + currentToast = null; this._setDevMode(!devOn); } }); @@ -160,15 +176,18 @@ class TopbarComponent { } // Toast helper — uses the project's notification system if loaded, else - // a quiet console echo so dev-mode UX never blocks page render. + // a quiet console echo so dev-mode UX never blocks page render. Returns + // the notification DOM element when the real system is available, so + // callers (the easter-egg countdown) can mutate the message in place + // instead of stacking new toasts on each click. _devToast(message, kind = 'info') { if (window.notificationSystem?.show) { - window.notificationSystem.show(message, kind); + return window.notificationSystem.show(message, kind); } else if (typeof window.showNotification === 'function') { - window.showNotification(message, kind); - } else { - console.log(`[dev-mode] ${message}`); + return window.showNotification(message, kind); } + console.log(`[dev-mode] ${message}`); + return null; } // Flip CFG_DEV_MODE via the standard config-update task (same path the