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.
This commit is contained in:
parent
4ec986da12
commit
9882d775e7
7 changed files with 83 additions and 7 deletions
|
|
@ -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<PodcastVoice | null>(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)]",
|
||||
)}
|
||||
>
|
||||
<SpeakerIcon className="size-4" />
|
||||
{iosLimited ? (
|
||||
<span
|
||||
aria-hidden
|
||||
className="absolute -right-0.5 -top-0.5 size-2.5 rounded-full bg-amber-400 ring-2 ring-[var(--page)]"
|
||||
/>
|
||||
) : null}
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
|
|
@ -416,6 +429,7 @@ export function PodcastPlayer({ episode }: { episode: Podcast }) {
|
|||
{!supported || unsupported ? (
|
||||
<p className="rounded-2xl border border-amber-300/50 bg-amber-50/70 px-3.5 py-3 text-[12.5px] leading-relaxed text-amber-800 dark:border-amber-400/25 dark:bg-amber-400/10 dark:text-amber-200">
|
||||
{t("podcast.noVoice")}
|
||||
{isAppleMobile() ? ` ${t("podcast.voiceIos")}` : ""}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
|
|
@ -533,9 +547,11 @@ export function PodcastPlayer({ episode }: { episode: Podcast }) {
|
|||
);
|
||||
})}
|
||||
<p className="px-1 pt-1 text-[11.5px] leading-relaxed text-[var(--text-subtle)]">
|
||||
{roboticOnly
|
||||
? t("podcast.voiceRobotic")
|
||||
: t("podcast.voiceNote")}
|
||||
{iosLimited
|
||||
? t("podcast.voiceIos")
|
||||
: roboticOnly
|
||||
? t("podcast.voiceRobotic")
|
||||
: t("podcast.voiceNote")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue