From 724a068bec0005d00fbd36d7ab22495e6dd06d12 Mon Sep 17 00:00:00 2001 From: Martin Dvorak Date: Fri, 16 Oct 2026 21:04:39 +0000 Subject: [PATCH] Aero music: the button works on the podcast player "The music thing button with the Frutiger Aero doesn't work on the podcasts." The podcast page is a full-screen overlay at z-45 and the control sat at z-40: visible through the transparent page, but every tap landed on the overlay. The control now takes a data-raised="player" position on /podcast/[id] - z-index 46, docked to the header's corner (top under the header pill, right mirroring the max-w-3xl geometry) where neither the transport bar nor the voice panel lives - and the transcript gained pt-32 so the episode card clears it. Study/review/exam feeds keep their "steps aside" behaviour. aero-music-test: a podcast section asserts the control exists, that elementFromPoint at its centre hits the button, and that a tap expands the credit bar. 31 checks. Version 0.7.9, SW v101. --- README.md | 11 ++++++++ app/globals.css | 9 ++++++ components/AeroMusic.tsx | 9 +++++- components/podcast/PodcastPlayer.tsx | 4 ++- package.json | 2 +- public/sw.js | 2 +- tests/aero-music-test.mjs | 41 ++++++++++++++++++++++++++++ 7 files changed, 74 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e76b92b..e30fa94 100644 --- a/README.md +++ b/README.md @@ -1492,6 +1492,17 @@ Things I chose while building, with the reasoning: into view as you type. No strings, so no i18n; the additive change sits on top of the merged scene-heat rework unchanged. +107. **The Aero soundtrack button rides above the podcast player.** "The music thing button with + the Frutiger Aero doesn't work on the podcasts": the podcast page is a full-screen overlay at + `z-45` and the music control sat at `z-40` — visible through the transparent page, but every + tap landed on the overlay. The control now takes a `data-raised="player"` position on + `/podcast/[id]`: `z-index: 46`, docked to the header's corner (`top` under the header pill, + `right` mirroring the `max-w-3xl` geometry) where neither the transport bar nor the voice panel + lives, and the transcript gained `pt-32` so the episode card clears it. The study/review/exam + feeds keep the "steps aside" behaviour — their bottom corners are spoken for. aero-music-test + grew a podcast section that asserts the control is there, that `elementFromPoint` at its centre + hits the button (nothing covers it) and that a tap expands the credit bar (31 checks). + 106. **When the browser has no speech engine at all.** "The podcasts don't even work on my Linux setup on Firefox": Firefox on Linux speaks through speech-dispatcher, and without it `speechSynthesis` is *present* (so `speechAvailable()` says yes) but empty and silent — diff --git a/app/globals.css b/app/globals.css index d8c4088..eaa8e5c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -933,6 +933,15 @@ body:has(.overlay-open) :is(.app-header, .segmented, .tabbar, .cookie-notice) { .aero-music[data-raised="wizard"] { bottom: calc(5.5rem + env(safe-area-inset-bottom, 0px)); } +/* The podcast player is a full-screen overlay (z-45): the music control has + to ride above it, and it moves to the header's corner — the transport and + the voice panel own the bottom of that screen. */ +.aero-music[data-raised="player"] { + top: calc(max(env(safe-area-inset-top), 0.75rem) + 3.9rem); + right: max(0.75rem, calc((100vw - 48rem) / 2 + 0.75rem)); + bottom: auto; + z-index: 46; +} body:has(.overlay-open) .aero-music, /* The one-time notice shares this corner; it wins while it is up. */ body:has(.cookie-notice) .aero-music { diff --git a/components/AeroMusic.tsx b/components/AeroMusic.tsx index 09549a2..0d3cf95 100644 --- a/components/AeroMusic.tsx +++ b/components/AeroMusic.tsx @@ -205,7 +205,14 @@ export function AeroMusic() { /* The composer sits at the bottom of the chat screen and the wizard has its own action bar; everywhere else the phone tab bar is the only neighbour. */ - const raised = pathname === "/chat" ? "chat" : pathname === "/create" || pathname === "/homework" ? "wizard" : undefined; + const raised = + pathname.startsWith("/podcast/") + ? "player" + : pathname === "/chat" + ? "chat" + : pathname === "/create" || pathname === "/homework" + ? "wizard" + : undefined; return (
diff --git a/components/podcast/PodcastPlayer.tsx b/components/podcast/PodcastPlayer.tsx index e2fe922..277cee9 100644 --- a/components/podcast/PodcastPlayer.tsx +++ b/components/podcast/PodcastPlayer.tsx @@ -585,7 +585,9 @@ export function PodcastPlayer({ episode }: { episode: Podcast }) { const index = Number(target.dataset.word); if (Number.isInteger(index)) jumpWord(index); }} - className="no-scrollbar min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pb-40 pt-24" + /* pt-32 clears the Aero music control, which rides above this overlay + in the header's corner while the theme is on. */ + className="no-scrollbar min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pb-40 pt-32" >
{naturalError ? ( diff --git a/package.json b/package.json index bb2bde2..31e1cd2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "freezmo", - "version": "0.7.8", + "version": "0.7.9", "private": true, "license": "GPL-3.0-only", "scripts": { diff --git a/public/sw.js b/public/sw.js index f46b661..76cf046 100644 --- a/public/sw.js +++ b/public/sw.js @@ -9,7 +9,7 @@ * * Bumping CACHE_VERSION is how a new deploy invalidates old assets. */ -const CACHE_VERSION = "freezmo-v100"; +const CACHE_VERSION = "freezmo-v101"; const SHELL_CACHE = `${CACHE_VERSION}-shell`; const ASSET_CACHE = `${CACHE_VERSION}-assets`; diff --git a/tests/aero-music-test.mjs b/tests/aero-music-test.mjs index e5d8bed..ad18cbc 100644 --- a/tests/aero-music-test.mjs +++ b/tests/aero-music-test.mjs @@ -208,6 +208,47 @@ async function stubSoundCloud(page) { await context.close(); } +/* 4b. the podcast player: the control rides above that overlay -------- */ +{ + const { context, page } = await makePage(browser, { width: 390, height: 844, deviceScaleFactor: 1, isMobile: true, hasTouch: true }); + await stubSoundCloud(page); + await seed(page, { + theme: "aero", + decks: [], + extra: { + "freezmo.podcasts.v1": JSON.stringify([ + { + id: "pod_music", + title: "While you listen", + summary: "", + language: "English", + sourceText: "x", + createdAt: 1, + updatedAt: 1, + sections: [{ heading: "One", text: "Look up on a clear day." }], + }, + ]), + }, + }); + await page.goto(`${BASE}/podcast/pod_music`, { waitUntil: "networkidle" }); + await page.waitForTimeout(1500); + + const circle = page.getByRole("button", { name: "Frutiger Aero music" }); + check("podcast: the music control is there while listening", (await circle.count()) === 1); + const reachable = await page.evaluate(() => { + const button = document.querySelector("[data-testid=aero-music] button"); + if (!button) return "missing"; + const box = button.getBoundingClientRect(); + const top = document.elementFromPoint(box.x + box.width / 2, box.y + box.height / 2); + return button.contains(top) || top === button ? "on-top" : `covered by ${top?.className?.toString().slice(0, 40)}`; + }); + check("podcast: nothing covers it (it rides above the overlay)", reachable === "on-top", reachable); + await circle.click(); + await page.waitForTimeout(400); + check("podcast: tapping it expands the credit bar", (await page.getByText("SoundCloud ·", { exact: false }).count()) >= 1); + await context.close(); +} + /* 5. immersive overlays step aside, the music keeps playing ----------- */ { const { context, page } = await makePage(browser, { width: 1280, height: 731 });