diff --git a/public/sw.js b/public/sw.js index a171699..93a123d 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'ns-v70'; +const CACHE = 'ns-v71'; const PRECACHE = ['/', 'index.html', 'manifest.json', 'icon.svg']; const SKIP_PATHS = ['/rest/stream', '/rest/download', '/config.js']; // Auth token params stripped from cache keys so artwork survives across sessions/token refreshes. diff --git a/src/main.js b/src/main.js index 62c1a25..30a9320 100644 --- a/src/main.js +++ b/src/main.js @@ -202,10 +202,12 @@ window.addEventListener('load', () => { // Skip when native is active: Media3 owns the system media session. if(navigator.mediaSession && !S._trackChanging && !window.NSBridge) navigator.mediaSession.playbackState = S.audio.paused ? 'paused' : 'playing'; - // Only auto-resume if audio was actually playing when the page hid (OS-killed buffer). - // If the user explicitly paused, _wasPlayingBeforeHide is false — don't restart. + // Only auto-resume after an INVOLUNTARY background stop (OS-killed buffer / frozen tab). + // Never restart if the user deliberately paused — including via the media notification while + // backgrounded (that routes through togglePlay → S._userPaused=true), which used to wrongly + // auto-resume on foreground because _wasPlayingBeforeHide was still true from hide-time. if(!S.audio.paused) return; - if(s && S.audio.src && _wasPlayingBeforeHide) S.audio.play().catch(() => { S._trackChanging = false; }); + if(s && S.audio.src && _wasPlayingBeforeHide && !S._userPaused) S.audio.play().catch(() => { S._trackChanging = false; }); } document.addEventListener('visibilitychange', () => { diff --git a/src/player/player.js b/src/player/player.js index 346a6bf..bc643ce 100644 --- a/src/player/player.js +++ b/src/player/player.js @@ -396,6 +396,7 @@ export function playQueue(songs, startIdx, opts = {}) { export function playCurrent() { if(S.qIdx < 0 || S.qIdx >= S.queue.length) return; + S._userPaused = false; // starting a track is intent to play — clear any stale pause intent const s = S.queue[S.qIdx]; if (HAS_BRIDGE) { @@ -661,8 +662,8 @@ export function cancelMusicAutoplay() { _playGen++; S.audio.pause(); } export function togglePlay() { if (HAS_BRIDGE) { _nsBridge('togglePlay'); return; } if(S.audio.src) { - if (S.audio.paused) { S.audio.play(); } - else { S.audio.pause(); _cancelScrobble(); } + if (S.audio.paused) { S._userPaused = false; S.audio.play(); } + else { S._userPaused = true; S.audio.pause(); _cancelScrobble(); } } } diff --git a/src/state.js b/src/state.js index 9688fca..a6c028f 100644 --- a/src/state.js +++ b/src/state.js @@ -15,6 +15,7 @@ export const S = { _scrubbing: false, _msPosLast: 0, _trackChanging: false, + _userPaused: false, // true when the user deliberately paused → foreground must NOT auto-resume queueOpen: false, // Now Playing overlay diff --git a/src/ui/stats.js b/src/ui/stats.js index e0ea290..7825a12 100644 --- a/src/ui/stats.js +++ b/src/ui/stats.js @@ -109,7 +109,7 @@ function _buildOverlay() { style="padding:6px 10px;color:var(--text2);font-size:18px;background:none;border:none;cursor:pointer">✕
-
+
`; // Delegated navigation: click an artist / album / track row → open its page. _overlay.querySelector('#stats-body').addEventListener('click', async e => { @@ -165,18 +165,26 @@ async function _load() { const auth = subsonicAuthParams(); if (!auth) { body.innerHTML = _errBox('Nicht angemeldet.'); return; } - body.innerHTML = `
Lade…
`; + // Only show "Lade…" on the very first open — on a period/tab switch keep the current content + // in place until the new data arrives, so the modal doesn't flash small→big (min-height also + // keeps its size stable). A per-request token guards against out-of-order responses. + if (!body.dataset.loaded) body.innerHTML = `
Lade…
`; + const token = String(Date.now()); + body.dataset.req = token; const { from, to, bucket } = _range(); const qs = `${auth}&from=${from}&to=${to}&tz=${_tzOffsetSec()}&bucket=${bucket}&limit=10`; try { const res = await fetch(`${S.cfg.scrobbleUrl}/scrobble/stats?${qs}`); + if (body.dataset.req !== token) return; // a newer request superseded this one if (!res.ok) { body.innerHTML = _errBox(`Fehler ${res.status}`); return; } const d = await res.json(); + if (body.dataset.req !== token) return; if (d.firstPlayedAt) { _firstPlayedAt = d.firstPlayedAt; _renderTabs(); } // apply back-bound _render(body, d); + body.dataset.loaded = '1'; } catch { - body.innerHTML = _errBox('Scrobble-Dienst nicht erreichbar.'); + if (body.dataset.req === token) body.innerHTML = _errBox('Scrobble-Dienst nicht erreichbar.'); } }