From 9882d775e7c98bf360ee109f3c33d669eac48d89 Mon Sep 17 00:00:00 2001 From: Martin Dvorak Date: Fri, 16 Oct 2026 13:51:12 +0000 Subject: [PATCH] Podcast voices: an iPhone note on where the other voices live Safari hands web pages one voice per language, whatever the iPhone has installed, so the picker cannot fix that on iOS. It now carries the way out instead: Accessibility -> Spoken Content -> Voices -> your language, download an Enhanced/Premium voice and make it preferred. The speaker button wears an amber dot while the note is waiting, and the voice-less banner appends the same guidance. Detection covers iPadOS too (maxTouchPoints, since it reports as a Mac). The suite fakes an iPhone user agent, a single Daniel voice and pins the dot and the note's actual steps (37 checks). Version 0.7.5, SW v97. --- README.md | 8 +++++- components/podcast/PodcastPlayer.tsx | 24 ++++++++++++++--- lib/i18n-dict.ts | 2 ++ lib/podcast-voices.ts | 13 ++++++++++ package.json | 2 +- public/sw.js | 2 +- tests/podcast-test.mjs | 39 ++++++++++++++++++++++++++++ 7 files changed, 83 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5e65982..ac203ba 100644 --- a/README.md +++ b/README.md @@ -1525,7 +1525,13 @@ Things I chose while building, with the reasoning: down, a male name preferred — "preferably male" was the ask), the player picks the best one automatically and offers a picker for everything in the episode's language, and the choice is remembered per language. The suite grew to 34 checks (voice ranking, the filtered picker, - persisting a pick, same-line silence, line following, manual-scroll suspension). + persisting a pick, same-line silence, line following, manual-scroll suspension). **Follow-up + 2 (0.7.5):** iOS is the special case the picker cannot fix — Safari hands web pages one voice + per language, so on an iPhone the panel now carries the settings path instead (Accessibility → + Spoken Content → Voices → language → Enhanced/Premium, set as preferred) and the speaker + button wears an amber dot while that note is waiting; the voice-less banner appends the same + guidance. Detected via the user agent (iPadOS included through `maxTouchPoints`); the suite + fakes an iPhone and pins the note, its steps and the dot (37 checks). 102. **An API key is not a login.** "When trying to use a different provider on the web app it glitches out completely" — a photo of the first-run gate with iOS's saved-password sheet laid diff --git a/components/podcast/PodcastPlayer.tsx b/components/podcast/PodcastPlayer.tsx index 8462625..53b7c51 100644 --- a/components/podcast/PodcastPlayer.tsx +++ b/components/podcast/PodcastPlayer.tsx @@ -13,6 +13,7 @@ import { savePodcastProgress } from "@/lib/podcasts"; import { bestVoice, getStoredVoice, + isAppleMobile, onlyRoboticVoices, platformVoices, setStoredVoice, @@ -103,6 +104,12 @@ export function PodcastPlayer({ episode }: { episode: Podcast }) { () => onlyRoboticVoices(voiceLocale, voiceList), [voiceLocale, voiceList], ); + /* iOS hands the page one voice per language; point at the settings that + change it (the note is the whole reason this section exists there). */ + const iosLimited = useMemo( + () => isAppleMobile() && availableVoices.length <= 1, + [availableVoices.length], + ); /** Latest position for the unmount/pause save, without re-creating callbacks. */ const latest = useRef(episode.progress ?? null); @@ -376,13 +383,19 @@ export function PodcastPlayer({ episode }: { episode: Podcast }) { aria-expanded={voiceOpen} title={voice?.name ?? t("podcast.voiceAria")} className={cn( - "grid size-10 shrink-0 place-items-center rounded-full transition", + "relative grid size-10 shrink-0 place-items-center rounded-full transition", voiceOpen ? "bg-[var(--nav-active-bg)] text-[var(--text)]" : "bg-[var(--surface-sunken)] text-[var(--text-muted)] hover:bg-[var(--hover)]", )} > + {iosLimited ? ( + + ) : null} ) : null}