feat(podcasts): queue context source + player order control — item 2.2
Foundation rule R2 (§10): the queue context is the only queue, and every producer emits the same shape. PodcastSequenceContext gains source (feed|collection), feed_id, and order; the feed producer sets them now — the collection producer (2.3) slots into the same shape without the player ever special-casing beyond showing/hiding controls. The player header gains the FEED-mode order control next to the "Ep N/M" counter: a dropdown (native/reverse/oldest/newest) plus a one-click reverse button. Both PUT the existing per-user feed order override — the SAME state the feed page writes, so flipping it in the player updates the feed page's dropdown too. The control renders only when source=feed: a collection queue's order is curated content (R1), never a listening preference. Behavior is hx-put + hx-swap="none" (response discarded) with a module-level htmx:afterRequest listener in audio.ts reloading the page — the server re-renders prev/next/index in the new order; templ decides what it looks like, JS only triggers the round trip (CSP-clean, no inline handlers). Pinned by TestPodcastPlayerOrderControl: the config JSON carries source/feed_id/order/episode_index; the control renders feed-mode; an order override re-renders with the dropdown reflecting it (data- json pins must match templ's " attribute escaping).
This commit is contained in:
1 parent
7bf0b1257d
commit
a90e7debfd
6 files changed
+279
-81
No files matched your search
@@ -116,6 +116,38 @@ templ AudioReader(user User, metadata ReaderMetadata) {
|
||||
<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>
|
||||
<!-- Order control (item 2.2, §10 R2): FEED-mode only — a
|
||||
collection queue's order is curated content, never a
|
||||
preference (hidden in collection mode). PUTs the same
|
||||
per-user override the feed page writes; hx-swap="none"
|
||||
discards the response and web/src/audio.ts reloads so
|
||||
the server re-renders prev/next/index in the new order. -->
|
||||
if metadata.PodcastSequence.Source == "feed" && metadata.PodcastSequence.FeedID != "" {
|
||||
<select
|
||||
id="player-order-select"
|
||||
name="order"
|
||||
aria-label="Episode order"
|
||||
class="input w-auto py-0.5 text-xs ml-2 cursor-pointer"
|
||||
hx-put={ "/api/podcasts/feeds/" + metadata.PodcastSequence.FeedID + "/order" }
|
||||
hx-swap="none"
|
||||
>
|
||||
<option value="native" selected?={ metadata.PodcastSequence.Order == "native" }>Feed order</option>
|
||||
<option value="reverse" selected?={ metadata.PodcastSequence.Order == "reverse" }>Feed order (reversed)</option>
|
||||
<option value="oldest" selected?={ metadata.PodcastSequence.Order == "oldest" }>Oldest first (pub date)</option>
|
||||
<option value="newest" selected?={ metadata.PodcastSequence.Order == "newest" }>Newest first (pub date)</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Reverse episode order"
|
||||
title="Reverse episode order (start from the feed's beginning)"
|
||||
class="rounded-full p-1.5 hover:bg-surface-hover transition-colors"
|
||||
hx-put={ "/api/podcasts/feeds/" + metadata.PodcastSequence.FeedID + "/order" }
|
||||
hx-vals='{"order": "reverse"}'
|
||||
hx-swap="none"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: var(--text-secondary)"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-2">
|
||||
|
||||
Reference in new issue
Block a user