From e462b54adfd7dfeb9fdbc5883ef78c0ba4b96e06 Mon Sep 17 00:00:00 2001 From: Honey Date: Sun, 26 Apr 2026 00:30:35 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20SW=20update=20banner=20=E2=80=94=20noti?= =?UTF-8?q?fy=20instead=20of=20force-reload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SW activate now posts SW_UPDATED message to all clients instead of calling client.navigate() (which was disruptive and unreliable) - injectPWA() calls reg.update() on every app open to eagerly check for a new SW rather than waiting for the 24h browser interval - showUpdateBanner() renders a persistent accent-bordered toast with version tag, a "Neu laden" button and a dismiss X - Banner sits above player bar, repositions on mobile above bottom-nav Co-Authored-By: Claude Sonnet 4.6 --- app.js | 35 ++++++++++++++++++++++++++++++++--- styles.css | 28 ++++++++++++++++++++++++++++ sw.js | 6 +++--- 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/app.js b/app.js index aa3f15d..a1c0a6f 100644 --- a/app.js +++ b/app.js @@ -1329,9 +1329,38 @@ async function loadArtists(){ // ─── PWA ─── function injectPWA(){ - if ('serviceWorker' in navigator) { - navigator.serviceWorker.register('sw.js').catch(() => {}); - } + if (!('serviceWorker' in navigator)) return; + navigator.serviceWorker.register('sw.js').then(reg => { + // Force-check for SW update every time the app opens + reg.update().catch(() => {}); + // New SW found while page is open (e.g. user left tab open overnight) + reg.addEventListener('updatefound', () => { + const sw = reg.installing; + if (!sw) return; + sw.addEventListener('statechange', () => { + // 'installed' + existing controller = update ready but waiting for skipWaiting + // With skipWaiting() this fires briefly; the SW_UPDATED message handles the banner + }); + }); + }).catch(() => {}); + + // SW posts SW_UPDATED after it activates and claims this client + navigator.serviceWorker.addEventListener('message', e => { + if (e.data?.type === 'SW_UPDATED') showUpdateBanner(e.data.version); + }); +} + +function showUpdateBanner(version) { + if (document.getElementById('update-banner')) return; + const b = document.createElement('div'); + b.id = 'update-banner'; + b.className = 'update-banner'; + b.innerHTML = ` + ✦ Update verfügbar${version ? ' ('+version+')' : ''} + + + `; + document.body.appendChild(b); } diff --git a/styles.css b/styles.css index 53525d1..fa90dcf 100644 --- a/styles.css +++ b/styles.css @@ -704,6 +704,34 @@ nav{flex:1;overflow-y:auto;padding:4px 10px} .mobile-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:140} .mobile-overlay.active{display:block} +/* ── UPDATE BANNER ── */ +.update-banner{ + position:fixed;bottom:calc(var(--player-h) + 20px);left:50%; + transform:translateX(-50%); + display:flex;align-items:center;gap:10px; + background:var(--surface);border:1px solid var(--accent); + border-radius:12px;padding:10px 14px; + box-shadow:0 8px 32px rgba(0,0,0,.55),0 0 0 1px rgba(232,160,32,.15); + z-index:202;white-space:nowrap; + animation:toastIn .3s ease; +} +.update-banner-text{font-size:13px;font-weight:600;color:var(--text)} +.update-banner-btn{ + background:var(--accent);color:#000;border:none; + padding:6px 14px;border-radius:7px; + font-size:12px;font-weight:700;cursor:pointer;font-family:var(--font); + transition:opacity .15s; +} +.update-banner-btn:hover{opacity:.85} +.update-banner-close{ + background:none;border:none;color:var(--text3); + cursor:pointer;font-size:15px;padding:0 2px;line-height:1; +} +.update-banner-close:hover{color:var(--text)} +@media(max-width:768px){ + .update-banner{bottom:calc(var(--player-h) + 80px + 12px)} +} + /* ── SEARCH page ── */ .search-section{margin-bottom:24px} .search-section-title{font-size:11px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px} diff --git a/sw.js b/sw.js index 8af5516..b3cc841 100644 --- a/sw.js +++ b/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'ns-v27'; +const CACHE = 'ns-v28'; const PRECACHE = ['/', 'index.html', 'styles.css', 'app.js', 'manifest.json', 'icon.svg']; const SKIP_PATHS = ['/rest/stream', '/rest/download', '/config.js']; @@ -12,8 +12,8 @@ self.addEventListener('activate', e => { caches.keys() .then(keys => Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))) .then(() => self.clients.claim()) - .then(() => self.clients.matchAll({type: 'window', includeUncontrolled: true})) - .then(clients => clients.forEach(c => c.navigate(c.url))) + .then(() => self.clients.matchAll({ type: 'window', includeUncontrolled: true })) + .then(clients => clients.forEach(c => c.postMessage({ type: 'SW_UPDATED', version: CACHE }))) ); });