diff --git a/plan.md b/plan.md index 601afd2..7d6a6be 100644 --- a/plan.md +++ b/plan.md @@ -600,7 +600,7 @@ offline. Verificare: typecheck, unit (cu teste noi), e2e neatins. -## Faza R5: recheck offline fara reenter + memoizare updateMediaSession +## Faza R5: recheck offline fara reenter + memoizare updateMediaSession [in PR #50] Zona sensibila iOS — ultima faza de logica, cu smoke pe device. @@ -622,7 +622,22 @@ tick-uri), e2e neatins, SMOKE PE DEVICE: telefon offline in error 5+ min cu ecranul blocat — widget-ul nu mai palpaie, bateria nu se scurge; revenire online → recovering → playing. -## Faza R6: startup mai usor (cloudinary precache) +## Faza R6: startup mai usor (cloudinary precache) [in PR #50] + +Nota de implementare (2026-07-04): la cererea lui Adrian, R5 + fix-ul +zombie (carrySound cu garda de generatie) + R6 merg intr-UN SINGUR PR +(#50). Abateri deliberate fata de planul initial al R6: +- Lista de precache NU se reduce la labels + statia curenta: e2e-ul (si + produsul) garanteaza ca posterele TUTUROR posturilor functioneaza + offline — reducerea ar fi schimbat comportamentul. Ramane lista + completa, doar amanata la requestIdleCallback (fallback setTimeout 3s + pe Safari). +- Pagina isi pastreaza cache.put-ul propriu: pe primul load SW-ul preia + controlul abia dupa activate/claim, iar fara put-ul paginii imaginile + fetch-uite inainte de claim ar ramane necache-uite. +- sw.js: put-ul de app-shell iese de pe calea raspunsului (waitUntil, ca + la cloudinary), /downloads/ (APK 2.4MB) nu se mai cache-uieste, bump + APP_CACHE v4 ca sa evacueze APK-ul din instalarile existente. - cloudinary.ts/main.ts: amana precacheStatusImages la `requestIdleCallback` (fallback setTimeout) si redu la labels + statia curenta (azi: 22 de diff --git a/src/js/main.ts b/src/js/main.ts index 83d59a4..81698b9 100644 --- a/src/js/main.ts +++ b/src/js/main.ts @@ -28,11 +28,19 @@ declare global { document.addEventListener("touchstart", function () { }, true); -// Pre-cache status images + station name images for offline use -precacheStatusImages([ +// Pre-cache status images + station name images for offline use — deferred +// to idle time so the ~22 image fetches don't compete with the stream +// connection and the sound-blob preloads at startup (time-to-audio first). +const runStatusImagePrecache = () => precacheStatusImages([ ...Object.values(LABELS), ...Array.from(radioSelect.options).map(o => o.text), ]); +if ('requestIdleCallback' in window) { + requestIdleCallback(runStatusImagePrecache, { timeout: 5000 }); +} else { + // Safari has no requestIdleCallback — a plain delay clears startup anyway. + setTimeout(runStatusImagePrecache, 3000); +} // Restore last station before anything reads selectedIndex const restoredStationIndex = getStoredStationIndex(radioSelect.options.length); diff --git a/src/js/mediaSession.ts b/src/js/mediaSession.ts index 8e63f33..f6911c7 100644 --- a/src/js/mediaSession.ts +++ b/src/js/mediaSession.ts @@ -91,6 +91,13 @@ function reassertPlaybackState() { loadingNoise.addEventListener('pause', reassertPlaybackState); errorNoise.addEventListener('pause', reassertPlaybackState); +// The presentation (metadata, poster, document title) is a pure function of +// (state, station title) — re-rendering it when neither changed is wasted +// work (and on iOS it re-fetches lock-screen artwork). Memoize on that key. +// Handler re-registration and playbackState are deliberately NOT memoized: +// iOS resets them out from under us (d798cc9), so they re-assert every call. +let lastRender: { state: RadioState; title: string } | null = null; + export const updateMediaSession = (newState: RadioState) => { const title = radioSelect.options[radioSelect.selectedIndex].text; const isIdle = newState === 'idle'; @@ -100,13 +107,17 @@ export const updateMediaSession = (newState: RadioState) => { const idleText = hasRestoredStation ? title : LABELS.appName; const displayText = isIdle ? idleText : isLoading ? LABELS.loading : hasError ? LABELS.error : title; + const changed = lastRender?.state !== newState || lastRender?.title !== title; + const artworkUrl = changed ? cloudinaryImageUrl(displayText, isLive) : ''; if ('mediaSession' in navigator) { - navigator.mediaSession.metadata = new MediaMetadata({ - title: isLoading ? `${LABELS.loading}${title}` : hasError ? `${LABELS.error} la încărcarea ${title}` : isIdle ? idleText : title, - artist: `${LABELS.appName}${isIdle && !hasRestoredStation ? '' : ` | ${title}`}`, - artwork: [{ src: cloudinaryImageUrl(displayText, isLive) }] - }); + if (changed) { + navigator.mediaSession.metadata = new MediaMetadata({ + title: isLoading ? `${LABELS.loading}${title}` : hasError ? `${LABELS.error} la încărcarea ${title}` : isIdle ? idleText : title, + artist: `${LABELS.appName}${isIdle && !hasRestoredStation ? '' : ` | ${title}`}`, + artwork: [{ src: artworkUrl }] + }); + } // Re-register ALL action handlers on every state transition. // iOS resets them when a different