From dad2b0b7304f8c9db0cffb7f2da93abc8135fccb Mon Sep 17 00:00:00 2001 From: Honey Date: Sun, 26 Apr 2026 01:02:56 +0200 Subject: [PATCH] fix: Android lock screen notification + radio mode + SW auto-update reliability - Set mediaSession.playbackState='playing'/'paused' so Android keeps the notification alive when the screen sleeps - Provide 96x96/300x300/512x512 artwork sizes for Android notification views - playQueue() now calls fillRadioQueue() immediately when radio mode is on, so the infinite queue works without toggling radio off/on - SW: add GET_VERSION/VERSION message handler for version-query fallback - SW registration: updateViaCache:'none' prevents browser HTTP-caching sw.js - Bump cache to ns-v30 Co-Authored-By: Claude Sonnet 4.6 --- app.js | 43 ++++++++++++++++++++++++------------------- sw.js | 8 +++++++- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/app.js b/app.js index a1c0a6f..6361ba7 100644 --- a/app.js +++ b/app.js @@ -116,8 +116,8 @@ window.addEventListener('load',()=>{ document.getElementById('srv-pass').addEventListener('keydown',e=>{if(e.key==='Enter')doConnect()}); audio.addEventListener('timeupdate',updateProgress); audio.addEventListener('ended',onEnded); - audio.addEventListener('play',()=>{setPlayIcon(true);document.getElementById('player-art').classList.add('spinning')}); - audio.addEventListener('pause',()=>{setPlayIcon(false);document.getElementById('player-art').classList.remove('spinning')}); + audio.addEventListener('play',()=>{setPlayIcon(true);document.getElementById('player-art').classList.add('spinning');if(navigator.mediaSession)navigator.mediaSession.playbackState='playing';}); + audio.addEventListener('pause',()=>{setPlayIcon(false);document.getElementById('player-art').classList.remove('spinning');if(navigator.mediaSession)navigator.mediaSession.playbackState='paused';}); previewAudio.addEventListener('timeupdate',updatePreviewProgress); previewAudio.addEventListener('ended',stopPreview); audio.volume=0.8; @@ -975,7 +975,7 @@ function dlSongLocal(s){ function downloadCurrent(){if(qIdx>=0&&queue[qIdx])dlSongLocal(queue[qIdx])} // ─── PLAYER ─── -function playQueue(songs,startIdx){queue=songs.map(s=>({...s}));qIdx=startIdx;playCurrent();renderQueue()} +function playQueue(songs,startIdx){queue=songs.map(s=>({...s}));qIdx=startIdx;playCurrent();renderQueue();if(radioMode)fillRadioQueue();} function playCurrent(){ if(qIdx<0||qIdx>=queue.length)return; const s=queue[qIdx]; @@ -1025,7 +1025,8 @@ function _syncNpDirect(s){ } function updateMediaSession(s){ if(!navigator.mediaSession)return; - navigator.mediaSession.metadata=new MediaMetadata({title:s.title,artist:s.artist||'',album:s.album||'',artwork:s.coverArt?[{src:coverUrl(s.coverArt,300),sizes:'300x300'}]:[]}); + const artwork=s.coverArt?[{src:coverUrl(s.coverArt,96),sizes:'96x96',type:'image/jpeg'},{src:coverUrl(s.coverArt,300),sizes:'300x300',type:'image/jpeg'},{src:coverUrl(s.coverArt,512),sizes:'512x512',type:'image/jpeg'}]:[]; + navigator.mediaSession.metadata=new MediaMetadata({title:s.title,artist:s.artist||'',album:s.album||'',artwork}); navigator.mediaSession.setActionHandler('play',togglePlay); navigator.mediaSession.setActionHandler('pause',togglePlay); navigator.mediaSession.setActionHandler('previoustrack',prevSong); @@ -1330,24 +1331,28 @@ async function loadArtists(){ // ─── PWA ─── function injectPWA(){ 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 + // Listen before registering so we never miss a message navigator.serviceWorker.addEventListener('message', e => { - if (e.data?.type === 'SW_UPDATED') showUpdateBanner(e.data.version); + if (e.data?.type === 'SW_UPDATED') { + localStorage.setItem('sw-version', e.data.version); + showUpdateBanner(e.data.version); + } + // Version query response — covers the race where SW activated before this listener + if (e.data?.type === 'VERSION') { + const prev = localStorage.getItem('sw-version'); + if (prev && prev !== e.data.version) showUpdateBanner(e.data.version); + localStorage.setItem('sw-version', e.data.version); + } }); + + // updateViaCache:'none' prevents the browser HTTP cache from hiding SW file changes + navigator.serviceWorker.register('sw.js', { updateViaCache: 'none' }).then(reg => { + reg.update().catch(() => {}); + // Query active SW version right now to catch updates that happened before page JS ran + const ctrl = navigator.serviceWorker.controller; + if (ctrl) ctrl.postMessage({ type: 'GET_VERSION' }); + }).catch(() => {}); } function showUpdateBanner(version) { diff --git a/sw.js b/sw.js index b486ad2..06c55be 100644 --- a/sw.js +++ b/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'ns-v29'; +const CACHE = 'ns-v30'; const PRECACHE = ['/', 'index.html', 'styles.css', 'app.js', 'manifest.json', 'icon.svg']; const SKIP_PATHS = ['/rest/stream', '/rest/download', '/config.js']; @@ -17,6 +17,12 @@ self.addEventListener('activate', e => { ); }); +self.addEventListener('message', e => { + if (e.data?.type === 'GET_VERSION') { + e.source.postMessage({ type: 'VERSION', version: CACHE }); + } +}); + self.addEventListener('fetch', e => { if (!e.request.url.startsWith(self.location.origin)) return; if (SKIP_PATHS.some(p => e.request.url.includes(p))) return;