From 54b20f57d7307afde505e67a03a8fe77e8789e1e Mon Sep 17 00:00:00 2001 From: Quick <31828688+Quick104@users.noreply.github.com> Date: Wed, 10 Jun 2026 09:08:18 -0400 Subject: [PATCH] =?UTF-8?q?feat(audiobooks):=20player=20QoL=20=E2=80=94=20?= =?UTF-8?q?shortcuts,=20smart=20rewind,=20per-book=20speed,=20volume,=20ch?= =?UTF-8?q?apter=20nav=20(#125)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings the web audiobook player to parity with best-in-class audiobook apps (Audiobookshelf, Audible, BookPlayer) on the quality-of-life axis, per docs/superpowers/plans/audiobook-player-qol.md: - Keyboard shortcuts active on every route while the player is mounted (space/K, arrow skips, volume, M, N/P chapters, shift+./, speed, E expand, Esc collapse), inert while a video session exists. - Configurable skip intervals with asymmetric defaults (back 10s, forward 30s) via a new player settings popover. - Speed control expanded to 0.5x-3x with a 0.05 stepper, presets, and per-book memory (LRU-capped, device-local). - Chapter prev/next transport buttons (prev restarts the chapter when more than 3s in, the universal player convention). - Volume/mute via the shared VolumeControl with a new theme-token "surface" tone; persistence shared with the video player. - Smart rewind: backs up 3-30s on resume scaled by pause length, plus a flat 10s on cold resume from the detail page; explicit seeks are never second-guessed. Toggleable in player settings. - Now Listening time label cycles total / remaining / remaining at current speed. Chapter flattening is deduplicated into lib/audiobooks/chapters.ts, the SkipIcon is shared between both player views, and new pure modules (smartRewind, chapter nav, prefs) come with unit tests. Co-authored-by: Claude Fable 5 --- .../superpowers/plans/audiobook-player-qol.md | 224 ++++++++++++++++++ web/src/lib/audiobooks/chapters.test.ts | 59 +++++ web/src/lib/audiobooks/chapters.ts | 53 +++++ .../ItemDetail/AudiobookContent.test.tsx | 3 +- web/src/pages/ItemDetail/AudiobookContent.tsx | 14 +- .../audiobooks/player/AudiobookPlayer.tsx | 15 ++ .../pages/audiobooks/player/MiniBar.test.tsx | 114 ++++++--- web/src/pages/audiobooks/player/MiniBar.tsx | 91 +++++-- .../audiobooks/player/NowListening.test.tsx | 58 ++--- .../pages/audiobooks/player/NowListening.tsx | 124 +++++++--- .../audiobooks/player/PlayerSettingsMenu.tsx | 115 +++++++++ web/src/pages/audiobooks/player/SkipIcon.tsx | 35 +++ .../pages/audiobooks/player/SpeedControl.tsx | 187 +++++++++++++++ .../audiobooks/player/playerTestUtils.ts | 58 +++++ .../audiobooks/player/smartRewind.test.ts | 40 ++++ .../pages/audiobooks/player/smartRewind.ts | 23 ++ .../useAudiobookKeyboardShortcuts.test.tsx | 113 +++++++++ .../player/useAudiobookKeyboardShortcuts.ts | 156 ++++++++++++ .../audiobooks/player/useAudiobookPlayback.ts | 155 ++++++++---- .../player/useAudiobookPrefs.test.ts | 116 +++++++++ .../audiobooks/player/useAudiobookPrefs.ts | 129 ++++++++++ web/src/player/components/CircleButton.tsx | 7 +- web/src/player/components/VolumeControl.tsx | 31 ++- web/src/utils/storage.ts | 4 + 24 files changed, 1752 insertions(+), 172 deletions(-) create mode 100644 docs/superpowers/plans/audiobook-player-qol.md create mode 100644 web/src/lib/audiobooks/chapters.test.ts create mode 100644 web/src/pages/audiobooks/player/PlayerSettingsMenu.tsx create mode 100644 web/src/pages/audiobooks/player/SkipIcon.tsx create mode 100644 web/src/pages/audiobooks/player/SpeedControl.tsx create mode 100644 web/src/pages/audiobooks/player/playerTestUtils.ts create mode 100644 web/src/pages/audiobooks/player/smartRewind.test.ts create mode 100644 web/src/pages/audiobooks/player/smartRewind.ts create mode 100644 web/src/pages/audiobooks/player/useAudiobookKeyboardShortcuts.test.tsx create mode 100644 web/src/pages/audiobooks/player/useAudiobookKeyboardShortcuts.ts create mode 100644 web/src/pages/audiobooks/player/useAudiobookPrefs.test.ts create mode 100644 web/src/pages/audiobooks/player/useAudiobookPrefs.ts diff --git a/docs/superpowers/plans/audiobook-player-qol.md b/docs/superpowers/plans/audiobook-player-qol.md new file mode 100644 index 00000000..d0c86ba0 --- /dev/null +++ b/docs/superpowers/plans/audiobook-player-qol.md @@ -0,0 +1,224 @@ +# Audiobook Player QoL Improvements + +Commands assume the repository root is the cwd. All paths are repository-relative. + +## Goal + +Bring the web audiobook player to feature parity with best-in-class audiobook apps +(Audiobookshelf, Audible, BookPlayer, Prologue) on the quality-of-life axis. The current +surface architecture (persistent mini bar + expandable Now Listening view, playback +survives navigation) is already the de-facto standard and stays as-is. + +Explicitly **out of scope**: Media Session / lock-screen integration, bookmarks (needs a +server endpoint — follow-up), offline/download, queue. + +## Features + +1. **Keyboard shortcuts** — space/K play-pause, arrow skips, volume, chapter nav, speed step. +2. **Configurable skip intervals** — asymmetric defaults (back 10s, forward 30s), user-tunable. +3. **Expanded speed control** — 0.5×–3.0× with fine stepping, presets, and per-book memory. +4. **Chapter prev/next buttons** — alongside the seconds-skip buttons. +5. **Volume control** — reuse the shared `VolumeControl`, persisted like the video player. +6. **Smart rewind** — auto-rewind on resume, scaled by how long playback was paused. +7. **Time remaining at current speed** — real-clock remaining display in Now Listening. + +No server/API changes are required; everything is web-frontend only, so no Android/Apple +client coordination is needed. (Per-book speed memory is device-local in v1; syncing it +server-side via profile settings is a noted follow-up.) + +## Existing seams (read these first) + +| File | Relevance | +| --- | --- | +| `web/src/pages/audiobooks/player/useAudiobookPlayback.ts` | Playback engine. `togglePlay` (~line 499), `skip` (~534), `setRate` (~541), returned API (~690+). All engine work lands here. | +| `web/src/pages/audiobooks/player/MiniBar.tsx` | Bottom bar. Hardcoded `SKIP_*_SECONDS = 30` and `PLAYBACK_RATES` at lines 10–12. | +| `web/src/pages/audiobooks/player/NowListening.tsx` | Full-screen view. Same hardcoded constants; dead "More" button (lines 50–56) becomes the settings menu trigger; remaining-time toggle at lines 33–37. | +| `web/src/pages/audiobooks/player/AudiobookPlayer.tsx` | Wrapper that owns mini/now-listening mode — mount point for the keyboard shortcuts hook. | +| `web/src/player/components/VolumeControl.tsx` | Existing slider + `getPersistedVolume`/`persistVolume` helpers (shared `player-volume`/`player-muted` storage keys). Styled for dark video overlay — needs a surface-toned variant. | +| `web/src/player/components/SpeedMenu.tsx` | Current preset-only menu (shared with video player). Audiobook gets a richer `SpeedControl`; video keeps `SpeedMenu` untouched. | +| `web/src/player/hooks/useKeyboardShortcuts.ts` | Video player's shortcut hook — pattern to mirror (input-field guard, single document listener). | +| `web/src/utils/storage.ts` | Typed localStorage wrapper — add new keys here, never call `localStorage` directly. | +| `web/src/player/components/SeekBar.tsx`, `CircleButton.tsx`, `SleepTimerMenu.tsx`, `ChaptersMenu.tsx` | Established player design language; reuse, don't fork. | + +## Design language + +These are additions to an existing, refined system — extend it, don't restyle it: + +- **Surfaces**: mini bar is a themed surface (`bg-background`, `border-t`, token colors). + Popovers anchored to it use the established dark-glass treatment + (`bg-black/90 backdrop-blur-sm rounded-lg shadow-xl`, white/75 text, `data-active` rows) + exactly as `SpeedMenu`/`SleepTimerMenu` do today. +- **Type**: time and rate values are always `font-mono`/`tabular-nums` so nothing shifts as + digits tick. The new speed readout (`1.45×`) must reserve width for two decimals. +- **Buttons**: all transport controls are `CircleButton` (`sm` secondary flanks, `md`/`lg` + primary center). Chapter prev/next use `SkipBack`/`SkipForward` lucide glyphs at the same + stroke weight (1.6) as the rotate icons, so the cluster reads as one family. +- **Micro-interactions**, small and purposeful: + - Skip buttons: a quick ~18° icon flick in the skip direction on press + (CSS transform transition, ~150ms ease-out) — confirms the action without a toast. + - Speed stepper: value crossfades (opacity 120ms) on change; the active preset chip gets + the same `bg-white/5 text-white` treatment as current menu rows. + - Smart rewind: when it fires, the seek bar playhead animates the small jump back rather + than teleporting (SeekBar already animates position; verify it holds for ≤30s deltas). +- **Accessibility**: every new control keeps the existing patterns — `aria-label`, + `aria-expanded`/`aria-haspopup` on triggers, roving focus + Escape in menus (copy from + `SleepTimerMenu`), `role="slider"` semantics from `VolumeControl`/`SeekBar`. + +## Phase 1 — Engine + preferences model + +### 1a. Preferences hook + +New `web/src/pages/audiobooks/player/useAudiobookPrefs.ts`: + +- Add storage keys: `AUDIOBOOK_SKIP_BACK`, `AUDIOBOOK_SKIP_FORWARD`, + `AUDIOBOOK_SMART_REWIND` (bool), `AUDIOBOOK_RATES` (JSON map) in `web/src/utils/storage.ts`. +- Exposes `{ skipBack, skipForward, smartRewind, setSkipBack, setSkipForward, setSmartRewind }`. +- Defaults: back **10s**, forward **30s** (the asymmetric convention — small "what did she + say?" hops back, bigger hops forward), smart rewind **on**. +- Allowed skip values: 5, 10, 15, 30, 45, 60, 90 (two digits max keeps `SkipIcon`'s inset + number legible). +- Per-book rate map: single JSON object `{ [contentId]: rate }` under `AUDIOBOOK_RATES`, + LRU-capped at 50 entries (store `{ rate, at }` and evict oldest on insert). + Helpers `getBookRate(contentId)` / `setBookRate(contentId, rate)` live here too. + +### 1b. Engine extensions (`useAudiobookPlayback.ts`) + +- **Volume/mute**: initialize from `getPersistedVolume()`; apply to the `