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:
1 parent
ace9e57d27
commit
b293741eb1
5 files changed
+387
-219
No files matched your search
@@ -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>
|
||||
|
||||
|
||||
Reference in new issue
Block a user