feat(reader): podcast episode prev/next on the transport row's outer buttons

User design (round 3): one set of big buttons does the big navigation.
The outer prev/next step buttons were chapter-only even on podcasts,
while episode nav hid as small icons in the top-left cluster. Now:

- Podcasts (sequence context present): the outer buttons navigate
  EPISODES via goToPrev/goToNextEpisode (progress pushed before the
  hop); disabled ghost buttons at the first/last episode keep the row
  shape. Embedded-chapter jumps stay on the Chapter pill's list.
- Books: unchanged chapter stepping.
- Keyboard (h/l shortcuts) and hardware media keys (MediaTrackPrev/
  Next) funnel through stepChapter, which is now sequence-aware —
  headset next-track = next episode on podcasts.
- The top-left episode icons are gone; the 'Ep N/M' counter stays.
- Help-modal labels say 'chapter, or episode on podcasts'.

Same icons both modes (extracted audioStepPrev/NextIcon partials);
books are untouched — no sequence context, byte-identical behavior.
This commit is contained in:
John O'Keefe committed 2026-10-30 12:46:36 -04:00
1 parent ace9e57d27
commit b293741eb1
5 files changed
+387 -219

No files matched your search

+85 -38
View File
@@ -34,6 +34,23 @@ func AudioReaderConfigJSON(metadata ReaderMetadata) string {
return string(b)
}
// audioStepPrevIcon / audioStepNextIcon: the transport row's outer
// step icons (chapter-step for books, EPISODE-step for podcasts — the
// same buttons serve both, round-3 user design 2026-10-30).
templ audioStepPrevIcon() {
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M19 20 9 12l10-8v16z"></path>
<path d="M5 19V5"></path>
</svg>
}
templ audioStepNextIcon() {
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5 4l10 8-10 8V4z"></path>
<path d="M19 5v14"></path>
</svg>
}
templ AudioReader(user User, metadata ReaderMetadata) {
<!DOCTYPE html>
<html lang="en">
@@ -91,22 +108,14 @@ templ AudioReader(user User, metadata ReaderMetadata) {
<path d="m12 19-7-7 7-7"></path>
</svg>
</button>
<!-- Podcast sequence (D7): episode prev/next + "Ep N of M".
Rendered only when the server supplied sequence context. -->
<!-- Podcast sequence (D7 + round-3 user design): "Ep N of M"
indicator. The episode prev/next live on the transport
row's outer buttons (same buttons books use for chapter
stepping) — not as separate icons here. -->
if metadata.PodcastSequence != nil {
<span class="text-xs tabular-nums ml-1" style="color: var(--text-secondary)">
Ep { fmt.Sprintf("%d", metadata.PodcastSequence.EpisodeIndex) }/{ fmt.Sprintf("%d", metadata.PodcastSequence.IndexTotal) }
</span>
if metadata.PodcastSequence.PrevURL != "" {
<a href={ metadata.PodcastSequence.PrevURL } class="rounded-full p-2 hover:bg-surface-hover transition-colors" style="color: var(--text-primary);" aria-label="Previous episode" title="Previous episode">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6 5h2v14H6zM20 5v14l-11-7z"></path></svg>
</a>
}
if metadata.PodcastSequence.NextURL != "" {
<a href={ metadata.PodcastSequence.NextURL } class="rounded-full p-2 hover:bg-surface-hover transition-colors" style="color: var(--text-primary);" aria-label={ "Next episode: " + metadata.PodcastSequence.NextTitle } title={ "Next: " + metadata.PodcastSequence.NextTitle }>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M16 5h2v14h-2zM4 5v14l11-7z"></path></svg>
</a>
}
}
</span>
<span class="inline-flex items-center gap-2">
@@ -334,19 +343,39 @@ templ AudioReader(user User, metadata ReaderMetadata) {
>0:00</button>
</div>
<div class="flex items-center justify-center gap-3 md:gap-5">
<button
type="button"
@click="stepChapter(-1)"
class="rounded-full p-2.5 hover:bg-surface-hover transition-colors"
aria-label="Previous chapter"
title="Previous chapter"
style="color: var(--text-primary);"
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M19 20 9 12l10-8v16z"></path>
<path d="M5 19V5"></path>
</svg>
</button>
<!-- Outer left button: books = previous chapter; podcasts =
previous EPISODE (round-3 user design: one set of big
buttons does the big navigation; keyboard + hardware
media keys route through stepChapter and follow). -->
if metadata.PodcastSequence != nil {
if metadata.PodcastSequence.PrevURL != "" {
<button
type="button"
@click="goToPrevEpisode()"
class="rounded-full p-2.5 hover:bg-surface-hover transition-colors"
aria-label="Previous episode"
title="Previous episode"
style="color: var(--text-primary);"
>
@audioStepPrevIcon()
</button>
} else {
<span class="rounded-full p-2.5 opacity-30 cursor-not-allowed" aria-disabled="true" title="No previous episode">
@audioStepPrevIcon()
</span>
}
} else {
<button
type="button"
@click="stepChapter(-1)"
class="rounded-full p-2.5 hover:bg-surface-hover transition-colors"
aria-label="Previous chapter"
title="Previous chapter"
style="color: var(--text-primary);"
>
@audioStepPrevIcon()
</button>
}
<button
type="button"
@click="skip(-1)"
@@ -399,19 +428,37 @@ templ AudioReader(user User, metadata ReaderMetadata) {
<span x-text="skipSeconds">10</span>
</span>
</button>
<button
type="button"
@click="stepChapter(1)"
class="rounded-full p-2.5 hover:bg-surface-hover transition-colors"
aria-label="Next chapter"
title="Next chapter"
style="color: var(--text-primary);"
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5 4l10 8-10 8V4z"></path>
<path d="M19 5v14"></path>
</svg>
</button>
<!-- Outer right button: books = next chapter; podcasts = next
EPISODE (disabled at the end of the feed). -->
if metadata.PodcastSequence != nil {
if metadata.PodcastSequence.NextURL != "" {
<button
type="button"
@click="goToNextEpisode(false)"
class="rounded-full p-2.5 hover:bg-surface-hover transition-colors"
aria-label={ "Next episode: " + metadata.PodcastSequence.NextTitle }
title={ "Next: " + metadata.PodcastSequence.NextTitle }
style="color: var(--text-primary);"
>
@audioStepNextIcon()
</button>
} else {
<span class="rounded-full p-2.5 opacity-30 cursor-not-allowed" aria-disabled="true" title="No next episode">
@audioStepNextIcon()
</span>
}
} else {
<button
type="button"
@click="stepChapter(1)"
class="rounded-full p-2.5 hover:bg-surface-hover transition-colors"
aria-label="Next chapter"
title="Next chapter"
style="color: var(--text-primary);"
>
@audioStepNextIcon()
</button>
}
</div>
</div>