feat(podcasts): hide played/downloaded toggle + episode text filter
Pocket Casts steal pulled forward from Phase 4 (§0 item 6): the episode list gains a client-side 'Filter episodes…' box and a 'Hide played/downloaded' toggle. Pure presentation — rows carry templ-rendered data-* attributes (downloaded, listen-pct, search text); the filter toggles DOM visibility with the 99.5% finishedThreshold. The toolbar lives OUTSIDE #episode-list so its state survives the list's polling swaps, and an htmx:afterSwap listener re-applies the active filter to freshly swapped rows. No query change, no server round trip, no new JS rendering surface (Alpine component method, CSP-safe).
This commit is contained in:
1 parent
5c545534c2
commit
f0f1eb6e8f
3 files changed
+473
-337
No files matched your search
@@ -9,6 +9,21 @@ package templates
|
||||
// the polling attributes render only then).
|
||||
|
||||
import "fmt"
|
||||
import "strings"
|
||||
|
||||
// boolAttr renders a boolean data attribute value.
|
||||
func boolAttr(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
// podcastPct renders a listen percentage for the row's data attribute
|
||||
// (one decimal; the client-side hide-played filter reads it).
|
||||
func podcastPct(p float64) string {
|
||||
return fmt.Sprintf("%.1f", p)
|
||||
}
|
||||
|
||||
templ PodcastDetailPage(user User, data PodcastDetailData) {
|
||||
<!DOCTYPE html>
|
||||
@@ -20,7 +35,7 @@ templ PodcastDetailPage(user User, data PodcastDetailData) {
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body x-data="podcastDetail" class={ "theme-" + user.Theme }>
|
||||
<body x-data="podcastDetail" x-init="initPodcastFilterPersistence()" class={ "theme-" + user.Theme }>
|
||||
@Header(user, "/podcasts")
|
||||
<main class="mx-auto container px-4 sm:px-6 lg:px-8 py-8">
|
||||
<a href="/podcasts" class="inline-flex items-center gap-1 text-sm mb-4 hover:underline" style="color: var(--text-secondary)">
|
||||
@@ -206,6 +221,27 @@ templ PodcastDetailPage(user User, data PodcastDetailData) {
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Episode-list filters (§0 item 6): pure client-local
|
||||
presentation state (Alpine; survives the list's htmx
|
||||
swaps because this toolbar lives OUTSIDE #episode-list).
|
||||
Hidden rows re-apply after every swap via the component's
|
||||
htmx:afterSwap listener. -->
|
||||
<div class="flex flex-wrap items-center gap-2 mb-2">
|
||||
<input
|
||||
type="search"
|
||||
class="input w-auto py-1.5 text-sm flex-1 min-w-[150px]"
|
||||
placeholder="Filter episodes…"
|
||||
aria-label="Filter episodes by title or description"
|
||||
x-ref="episodeFilterInput"
|
||||
@input="applyEpisodeFilter($event)"
|
||||
/>
|
||||
<label class="flex items-center gap-1.5 text-sm cursor-pointer select-none" style="color: var(--text-secondary)">
|
||||
<input type="checkbox" class="cursor-pointer" @change="applyEpisodeFilter($event)" x-ref="hideFinished"/>
|
||||
Hide played/downloaded
|
||||
</label>
|
||||
<span class="text-xs hidden" x-ref="filterEmptyNote" style="color: var(--text-secondary)">No episodes match the filter.</span>
|
||||
</div>
|
||||
|
||||
<!-- Episode list (self-refreshing htmx partial). Pick the
|
||||
polling variant at page-render time when downloads are
|
||||
active — otherwise a fresh page load mid-download
|
||||
@@ -268,7 +304,13 @@ templ PodcastEpisodeListPolling(data PodcastDetailData) {
|
||||
}
|
||||
|
||||
templ PodcastEpisodeRowView(ep PodcastEpisodeRow) {
|
||||
<div class="card p-3 flex items-center gap-3">
|
||||
<div
|
||||
class="card p-3 flex items-center gap-3"
|
||||
data-episode-row="true"
|
||||
data-downloaded={ boolAttr(ep.Downloaded) }
|
||||
data-listen-pct={ podcastPct(ep.ListenPct) }
|
||||
data-search-text={ strings.ToLower(ep.Title + " " + ep.Description) }
|
||||
>
|
||||
<span class="text-xs font-mono w-10 shrink-0 text-right" style="color: var(--text-secondary)">
|
||||
{ podcastInt(ep.Position) }
|
||||
</span>
|
||||
|
||||
Reference in new issue
Block a user