feat: SW update banner — notify instead of force-reload
- 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 <noreply@anthropic.com>
This commit is contained in:
1 parent
15959b45eb
commit
e462b54adf
3 files changed
+63
-6
No files matched your search
@@ -1329,9 +1329,38 @@ async function loadArtists(){
|
|||||||
|
|
||||||
// ─── PWA ───
|
// ─── PWA ───
|
||||||
function injectPWA(){
|
function injectPWA(){
|
||||||
if ('serviceWorker' in navigator) {
|
if (!('serviceWorker' in navigator)) return;
|
||||||
navigator.serviceWorker.register('sw.js').catch(() => {});
|
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 = `
|
||||||
|
<span class="update-banner-text">✦ Update verfügbar${version ? ' <span style="opacity:.55;font-size:10px">('+version+')</span>' : ''}</span>
|
||||||
|
<button class="update-banner-btn" onclick="window.location.reload()">Neu laden</button>
|
||||||
|
<button class="update-banner-close" onclick="document.getElementById('update-banner').remove()">✕</button>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+28
@@ -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{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:140}
|
||||||
.mobile-overlay.active{display:block}
|
.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 page ── */
|
||||||
.search-section{margin-bottom:24px}
|
.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}
|
.search-section-title{font-size:11px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px}
|
||||||
@@ -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 PRECACHE = ['/', 'index.html', 'styles.css', 'app.js', 'manifest.json', 'icon.svg'];
|
||||||
const SKIP_PATHS = ['/rest/stream', '/rest/download', '/config.js'];
|
const SKIP_PATHS = ['/rest/stream', '/rest/download', '/config.js'];
|
||||||
|
|
||||||
@@ -12,8 +12,8 @@ self.addEventListener('activate', e => {
|
|||||||
caches.keys()
|
caches.keys()
|
||||||
.then(keys => Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k))))
|
.then(keys => Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k))))
|
||||||
.then(() => self.clients.claim())
|
.then(() => self.clients.claim())
|
||||||
.then(() => self.clients.matchAll({type: 'window', includeUncontrolled: true}))
|
.then(() => self.clients.matchAll({ type: 'window', includeUncontrolled: true }))
|
||||||
.then(clients => clients.forEach(c => c.navigate(c.url)))
|
.then(clients => clients.forEach(c => c.postMessage({ type: 'SW_UPDATED', version: CACHE })))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user