feat(podcasts): chain playback + Continue section — sequence mode in the web player (phase 2 core)
D7 chain-by-default, delivered with minimal player surgery: - ReaderMetadata gains PodcastSequence: the neighboring DOWNLOADED episodes in the caller's effective order (D5 override applied, feed-native fallback; computed in the reader route for source='podcast_episode' items — RLS already scoped the reads; gaps never enter the chain, invariant 6). ?single=1 escapes chaining (the 'play single episode' affordance). - Audio reader renders the sequence into body[data-json] (AudioReaderConfigJSON — books produce the identical shape minus the sequence key, so the reader path is unchanged for them) and an 'Ep N/M' indicator + prev/next episode controls in the header. - audio.ts: the last-track 'ended' handler chains into the next episode's reader page (final progress push precedes navigation); back button exits episodes to their feed page. Books are untouched — no sequence context, zero behavior change. Day-one chaining is page-navigation based (progress persists; position restores). Continue aggregation (2.5): GetPodcastContinueForUser (in-progress episodes, most-recent-first, ≥95% excluded) feeds a Continue section on /podcasts — sorted most recently listened, cards show 'Ep N · title' — ahead of All Feeds. Cards prefer the continue line over the description.
This commit is contained in:
1 parent
2235b00c59
commit
28420d56ed
14 files changed
+740
-174
No files matched your search
@@ -67,7 +67,7 @@ templ PodcastsPage(user User, data PodcastsPageData, currentPath string) {
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Add an RSS feed to start listening — episodes land here and download on demand.</p>
|
||||
</div>
|
||||
} else {
|
||||
@PodcastsGrid(data.Feeds)
|
||||
@PodcastsSections(data)
|
||||
}
|
||||
}
|
||||
</main>
|
||||
@@ -115,6 +115,22 @@ templ PodcastsFilterBar(data PodcastsPageData) {
|
||||
}
|
||||
}
|
||||
|
||||
// PodcastsSections renders the Continue section (feeds with in-progress
|
||||
// episodes, most recently listened first — plan 2.5) ahead of the
|
||||
// alphabetical All Feeds grid.
|
||||
templ PodcastsSections(data PodcastsPageData) {
|
||||
if len(data.ContinueFeeds) > 0 {
|
||||
<h2 class="text-lg font-bold mb-3 mt-2" style="color: var(--text-primary)">Continue</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 mb-8">
|
||||
for _, feed := range data.ContinueFeeds {
|
||||
@PodcastFeedCardView(feed)
|
||||
}
|
||||
</div>
|
||||
<h2 class="text-lg font-bold mb-3" style="color: var(--text-primary)">All Feeds</h2>
|
||||
}
|
||||
@PodcastsGrid(data.Feeds)
|
||||
}
|
||||
|
||||
templ PodcastsGrid(feeds []PodcastFeedCard) {
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
for _, feed := range feeds {
|
||||
@@ -151,7 +167,12 @@ templ PodcastFeedCardView(feed PodcastFeedCard) {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
if feed.Description != "" {
|
||||
if feed.HasContinue {
|
||||
<p class="text-xs font-medium line-clamp-1" style="color: var(--accent)">
|
||||
@Icon("book-open", "h-3.5 w-3.5 inline")
|
||||
{ " " + podcastInt(feed.ContinueEpisode) + ". " + feed.ContinueTitle }
|
||||
</p>
|
||||
} else if feed.Description != "" {
|
||||
<p class="text-xs line-clamp-2" style="color: var(--text-secondary)">{ feed.Description }</p>
|
||||
}
|
||||
<div class="mt-auto flex flex-wrap items-center gap-1.5 text-xs">
|
||||
|
||||
Reference in new issue
Block a user