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:
1 parent
3129333f9a
commit
1d98b273e7
5 files changed
+21
-9
No files matched your search
+1
-1
@@ -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
@@ -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', () => {
|
||||
|
||||
@@ -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(); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user