fix(web player + stats modal): no auto-resume after user pause; stable stats modal

Player: opening the PWA no longer auto-plays when the user had paused. Pausing via
the media notification while backgrounded routes through togglePlay, which now sets
S._userPaused; _onForeground only auto-resumes an INVOLUNTARY background stop
(_wasPlayingBeforeHide && !_userPaused). playCurrent clears the flag on a new track.
Involuntary-stop/freeze recovery on foreground is preserved.

Stats modal: no more small→big flash when switching period/tab — the body keeps a
stable min-height and retains its current content until the new data arrives (only
the first open shows 'Lade…'); a per-request token drops out-of-order responses.
SW cache -> ns-v71.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 4.8 committed 2026-08-10 18:06:18 +02:00
1 parent 3129333f9a
commit 1d98b273e7
5 files changed
+21 -9

No files matched your search

+1 -1
View File
@@ -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.
+5 -3
View File
@@ -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', () => {
+3 -2
View File
@@ -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(); }
}
}
+1
View File
@@ -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
+11 -3
View File
@@ -109,7 +109,7 @@ function _buildOverlay() {
style="padding:6px 10px;color:var(--text2);font-size:18px;background:none;border:none;cursor:pointer">✕</button>
</div>
<div class="stats-tabs" id="stats-tabs" style="display:flex;gap:6px;padding:12px 18px 0"></div>
<div class="stats-body" id="stats-body" style="padding:16px 18px;overflow-y:auto"></div>
<div class="stats-body" id="stats-body" style="padding:16px 18px;overflow-y:auto;min-height:62vh"></div>
</div>`;
// 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 = `<div style="text-align:center;color:var(--text2);padding:40px 0">Lade…</div>`;
// 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 = `<div style="text-align:center;color:var(--text2);padding:40px 0">Lade…</div>`;
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.');
}
}