package templates // Feed detail page (plan task 1.9, D5/D7/D8/D19): hero (artwork, // metadata, admin cluster), action row (Download All, Refresh, order // override, subscription policy, admin delete), and the episode list // with the download lifecycle inline. The episode list is a self- // refreshing htmx partial: it re-fetches on the podcast:changed event // AND polls every 4s ONLY while downloads are active (server decides — // the polling attributes render only then). templ PodcastDetailPage(user User, data PodcastDetailData) { { data.Feed.DisplayTitle() } - Bookhoard @Header(user, "/podcasts")
@Icon("arrow-left", "h-4 w-4") Podcasts
if data.Feed.ArtworkPath != "" { { } else { @Icon("podcast", "h-12 w-12") }
{ data.Feed.TypeLabel() } if data.Feed.IsComplete { complete } if data.Feed.Language != "" { { data.Feed.Language } }

{ data.Feed.DisplayTitle() }

if data.Feed.Host != "" || data.Feed.Publisher != "" {

if data.Feed.Host != "" { { "Host: " + data.Feed.Host } } if data.Feed.Publisher != "" && data.Feed.Publisher != data.Feed.Host { { " · Publisher: " + data.Feed.Publisher } }

} if data.Feed.Description != "" {

{ data.Feed.Description }

} if len(data.Feed.Tags) > 0 {
for _, t := range data.Feed.Tags { { t } }
}

{ podcastInt(data.Feed.DownloadedN) + " of " + podcastInt(data.Feed.EpisodeTotal) + " episodes downloaded" } if data.Feed.LastFetched != "" { { " · last checked " + data.Feed.LastFetched } }

if data.IsSerial && data.Order == "native" { } if data.Feed.AutoDownload == "newest" { } if data.IsAdmin { }
if data.GapCount > 0 {
@Icon("circle", "h-5 w-5")

{ podcastInt(data.GapCount) + " episode(s) not downloaded in the middle of this feed" }

Playback skips them and keeps going — use Download All to fill the gaps.

} @PodcastEpisodeList(data)
} // PodcastEpisodeList is the refresh unit: re-fetched on the // podcast:changed event (download/refresh/order endpoints fire it) and // polled every 4s only while the server says downloads are active. templ PodcastEpisodeList(data PodcastDetailData) {

Episodes

if len(data.Episodes) == 0 {
No episodes yet — hit Refresh to check the feed.
} else {
for _, ep := range data.Episodes { @PodcastEpisodeRowView(ep) }
}
} // PodcastEpisodeListPolling renders the poll-active variant of the list // container (same content; the hx-trigger adds the 4s poll + load). The // SSR episode route picks one of the two by download activity. templ PodcastEpisodeListPolling(data PodcastDetailData) {

Episodes

for _, ep := range data.Episodes { @PodcastEpisodeRowView(ep) }
} templ PodcastEpisodeRowView(ep PodcastEpisodeRow) {
{ podcastInt(ep.Position) }
{ ep.Title } if ep.DownloadState == "failed" { { ep.StateBadge() } } else if ep.DownloadState == "queued" || ep.DownloadState == "downloading" { { ep.StateBadge() } } else if ep.DownloadState == "pending" { { ep.StateBadge() } }
if ep.DownloadError != "" {

{ ep.DownloadError }

}

{ ep.Date } if ep.Duration != "" { { " · " + ep.Duration } } if ep.Size != "" { { " · " + ep.Size } }

if ep.Downloaded { @Icon("book-open", "h-4 w-4") Play } else if ep.DownloadState == "queued" || ep.DownloadState == "downloading" { @Icon("refresh", "h-4 w-4") } else { }
} // downloadButtonLabel: pending → Download; failed → Retry (the reason // text above it explains what went wrong — D8/D19 inline failure UX). func downloadButtonLabel(state string) string { if state == "failed" { return "Retry" } return "Download" } func downloadButtonTitle(state string) string { if state == "failed" { return "Retry the download" } return "Download this episode" }