Commit Graph

17 Commits

Author SHA1 Message Date
d7aec4e810 fix(memory): bound visualizer PCM cache and feed episode cache
The visualizer's PCM cache decoded the entire episode into RAM (22050 Hz
mono s16 ~160 MB/hr of audio) and held it until stop() — a 3-hour episode
pinned ~500 MB and long-form content hit 2.5 GB. The 4x decode also pulled
the whole remote file even when only minutes were listened to.

- audio-pcm-cache: sliding window around the playback position — the
  decode head caps at maxAheadSec (600s) ahead of the cursor, segments
  older than keepBehindSec (300s) are pruned, and the tail refills as
  playback advances. Steady state ~40 MB regardless of episode length;
  a backward seek past the window restarts a segment there (the existing
  seek-hole mechanism, no new failure mode).
- feed: cap the full-parse episode cache at 1000 episodes/feed so
  archive-heavy subscriptions can't pin their entire history in RAM;
  the visible list stays bounded by the user's cache preference and
  fetch-more keeps working within the ceiling.
- tests: pin the new head-cap and prune contracts (8/8 in
  audio-pcm-cache.test.ts; full suite 193 pass).

Also includes the in-flight cleanup/refactor pass (cover-art resolve
helper, page and comment tightening, ESLint config removal).
2026-08-12 21:02:19 -04:00
deac6081ca feat(feed): bound feed lifecycle to 30-day window with nonblocking refresh
Persisted feeds keep only episodes from the last 30 days (plus completed
downloads); older episodes live in volatile memory and survive refreshes
via union merge, with per-feed in-memory caches capped at 500. Refresh
batches run at FETCH_CONCURRENCY=4 with per-feed incremental apply (no
Promise.all barrier), config.json writes are trailing-edge debounced
(250ms, immediate flushPendingSave for unsubscribes), and cold
fetch-more refetches abort at FETCH_TIMEOUT_MS. A shared activity store
powers a global top-right indicator covering refresh, fetch-more,
subscribe, search, and downloads.

Also includes the in-flight incremental RSS parsing (chunked with
event-loop yields) and refresh spinner work this tree already carried.
2026-08-12 10:13:20 -04:00
0c3506beb5 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
2026-08-11 00:50:13 -04:00
dc855ab8a0 feat(shell): hold now-playing marquee at start between scroll passes
Previously the marquee looped continuously, cycling back to the
start the moment the text finished. Now it holds at the start for
SCROLL_HOLD_MS (10s), scrolls one pass at SCROLL_STEP_MS per char,
then holds again.
2026-08-10 20:57:08 -04:00
d2c46631ef feat(shell): now-playing marquee in status bar (podcast — episode)
The bottom-bar now-playing segment shows '<podcast> — <episode>' (custom name
when set, feed resolved like advanceEpisode), takes the full remaining status-bar
width, and marquee-scrolls on a 300ms timer when the text overflows instead of
truncating at 40 chars. Static when it fits; no interval runs on fit/narrow bars.
2026-08-10 16:50:03 -04:00
491a736c32 Restore center-pane borders, move title to top-left slot
- Current pane gets muted left/right borders only (no full box, no accent
  ring); border colors are passed only when a border is requested, since
  opentui flips borderless boxes to bordered when borderColor is supplied.
- Remove the Up / <current tab> / Detail titles above the panes; the current
  pane's title now renders once, top-left in the parent column's header slot.
- Drop the parentLabel/previewLabel props from PaneRow and all callers.
- Remove the tab/depth indicator from the bottom-left of the status bar.
- Tests measure column widths from the border glyphs and assert the
  left/right edges render muted regardless of focus.
2026-08-10 09:00:24 -04:00
25307f83e9 fix: up highlight made legible for transparent bg, bring back mouse nav 2026-08-09 22:21:30 -04:00
db285530b6 feat: private feeds, all input fields supersede keyboard nav 2026-08-09 15:39:02 -04:00
2abdbaa4e9 cleaning up code 2026-08-09 09:54:52 -04:00
13a31aabdc docs: mark AUR package as pending registration reopen 2026-08-08 07:09:07 -04:00
85cb9fba26 ui cleanup 2026-08-07 13:38:32 -04:00
c8d29ed59d fix navigation flow 2026-08-07 00:28:24 -04:00
b24c83711d refactor(keybinds): extract unified action dispatcher
Move the keybind router out of Shell into a reusable createDispatcher
in @/utils/dispatch, covering tabs (digits 1-6, [/]), h/l depth
drill/pop + fixed-pane swipe, modes, audio transport, quit/help, and
command, delegating pane/list actions over the nav.action event bus.

Shell now renders the active page full-width with a bottom status bar
carrying the tab strip; the old sidebar pane and its leftover sidebar
action handling are removed. Adds tests covering the dispatcher's
routing behavior.
2026-07-31 19:19:20 -04:00
7c7d487ca6 refactor: move tab strip from left sidebar to bottom status bar
- Remove the left tab sidebar containing tab labels and now-playing
- Render tabs as compact labels in the command bar row instead
- Move now-playing display next to the selection count in the status bar
- All tabs remain functional; clicking a tab label switches the active tab
2026-07-31 19:05:43 -04:00
b1bb9d9a1e refactor: rearchitect navigation model with pure store and no sidebar pane
- Extract navigation state and logic into navigation-store.ts for
  testability and separation of concerns
- NavigationContext now only wraps the store as a Solid provider
- Remove SIDEBAR_PANE from the model; depth-tabs have a single
  focusable content pane (center/current column)
- Split LayerGraph and page imports into layer-graph.ts so the
  navigation utils stay free of JSX (unit-testable)
- Update Shell keybind dispatch: remove sidebar pane handling,
  simplify swipe to fixed-pane tabs only, clarify depth-tab h/l
- Add nav-model.test.ts covering depth stack, tab/pane focus, and
  selection semantics
2026-07-31 10:24:45 -04:00
97b2f61e5f pre-ui-rearch 2026-07-31 01:05:32 -04:00
d8f11040bc start revive 2026-07-30 21:25:03 -04:00