feat(search): episode search scope with tab toggle

Search now covers individual episodes, not just shows: the iTunes
Search API (entity=podcastEpisode) matches episode titles and show
notes, so a guest or topic finds the episodes they appear in across
shows. Enter on an episode result subscribes to the parent show.

- types: SearchResult becomes a kind-discriminated union
  (podcast | episode); EpisodeSearchResult carries the parent show so
  existing consumers compile unchanged
- source-searcher: searchEpisodesByType (RSS/CUSTOM return []),
  buildItunesEpisodeUrl, cleanDescription (HTML -> text),
  mapItunesEpisodeResult (episode id/duration ms->s/audioUrl, reuses
  mapItunesResult so delisted shows keep a directoryUrl)
- search: searchEpisodes with an 'episode' cache/dedupe namespace so
  shows and episodes for the same query never mix
- stores/search: scope signal (podcast | episode) persisted to
  podtui_search_scope; search() branches on scope
- SearchPage: Shows/Episodes pills row with 'tab to toggle', scope-
  aware placeholder/empty state/result rows (episode row = title +
  Show · date) and preview; toggling re-runs the current query
- keybinds: search-scope-toggle bound to tab (keybinds.jsonc AND the
  runtime DEFAULT_KEYBINDS merge so the binding exists for users with
  a pre-existing config file); while the input is focused the Shell
  router never sees Tab, so the input handles it via onKeyDown +
  preventDefault (no double-toggle: the router path only fires when
  the input is defocused)
- Shell help overlay documents [tab] shows/episodes
This commit is contained in:
2026-08-11 00:50:13 -04:00
parent ef9fc13aaa
commit 0c3506beb5
7 changed files with 276 additions and 73 deletions

View File

@@ -71,6 +71,7 @@ export const PAGE_ACTIONS: ReadonlySet<KeybindActionName> =
"open",
"open-interactive",
"search",
"search-scope-toggle",
"filter",
"sort",
"toggle-hidden",

View File

@@ -60,6 +60,7 @@ const DEFAULT_KEYBINDS: KeybindsResolved = {
help: ["~", "f1"],
// list ops
search: ["s"],
"search-scope-toggle": ["tab"],
filter: ["f"],
sort: [","],
"toggle-hidden": ["."],