Skip to content

Add background playback toggle and fix tab-switch audio overlap#79

Open
ru-aish wants to merge 2 commits intorichardr1126:mainfrom
ru-aish:feat/background-playback-toggle
Open

Add background playback toggle and fix tab-switch audio overlap#79
ru-aish wants to merge 2 commits intorichardr1126:mainfrom
ru-aish:feat/background-playback-toggle

Conversation

@ru-aish
Copy link

@ru-aish ru-aish commented Feb 11, 2026

Summary:

  • add persisted setting keepPlayingInBackground (default true)
  • expose Keep playing in background toggle in document settings and Settings > Documents
  • wire TTS background behavior to config instead of always pausing on tab hide
  • prevent duplicate/overlapping playback on tab switches by guarding play/pause with Howl.playing()

Why:

  • allow uninterrupted playback while switching tabs
  • fix overlap bug seen after tab visibility changes

Validation:

  • Docker build completed
  • app started successfully
  • Kokoro endpoint reachable from OpenReader container

Summary by CodeRabbit

  • New Features
    • Added "Keep playing in background" toggle in Settings and Document Settings (visible for non‑HTML documents). When enabled, audio continues playing when the app is backgrounded or device locked and resumes/preloads more reliably.
    • Default behavior: the setting is enabled by default.

@vercel
Copy link

vercel bot commented Feb 11, 2026

@ru-aish is attempting to deploy a commit to the richardr1126's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Feb 11, 2026

📝 Walkthrough

Walkthrough

Adds a new boolean config flag keepPlayingInBackground, exposes it in settings UIs, and updates background playback logic so audio may continue when the document loses focus if the flag is enabled.

Changes

Cohort / File(s) Summary
Config types & storage
src/types/config.ts, src/lib/dexie.ts
Added keepPlayingInBackground: boolean to AppConfigValues, added default true, and included the field in DB migration/row construction.
Config provider
src/contexts/ConfigContext.tsx
Propagated keepPlayingInBackground through ConfigProvider and included it in the context value returned by useConfig().
Settings UIs
src/components/DocumentSettings.tsx, src/components/SettingsModal.tsx
Added checkbox controls to toggle keepPlayingInBackground (document-level UI shows it only for non-HTML documents); wired to updateConfigKey('keepPlayingInBackground', ...).
TTS & background playback
src/contexts/TTSContext.tsx, src/hooks/audio/useBackgroundState.ts
Integrated keepPlayingInBackground into background/visibility logic. useBackgroundState API changed: replaced playAudio prop with keepPlayingInBackground: boolean; pause/resume behavior now conditional on the flag and uses guarded Howl calls.
Other
(no exported signature removals)
No exported function/class signatures removed; public surface expanded to include the new config field.

Sequence Diagram

sequenceDiagram
    participant User
    participant SettingsUI as "Settings UI"
    participant Config as "ConfigContext"
    participant TTS as "TTSContext"
    participant BG as "useBackgroundState"
    participant Audio

    User->>SettingsUI: Toggle "Keep playing in background"
    SettingsUI->>Config: updateConfigKey('keepPlayingInBackground', value)
    Config->>TTS: context value updated
    TTS->>BG: keepPlayingInBackground observed

    Note over User,Audio: Document visibility changes
    User->>Audio: Document hidden (visibilitychange)
    Audio->>BG: notify visibility change

    alt keepPlayingInBackground == true
        BG->>Audio: Do not pause — continue playback
    else keepPlayingInBackground == false
        BG->>Audio: Pause playback
    end

    Note over User,Audio: Document returns to foreground
    User->>Audio: Document visible
    Audio->>BG: notify visibility change
    BG->>Audio: Resume if conditions met
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰
A tiny hop, the speakers hum,
Even when the windows run,
If you tick the box, the songs stay near,
I twitch my nose and lend an ear,
Keep playing on — hooray! 🎶

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the two main changes: adding a background playback toggle and fixing tab-switch audio overlap, both of which are evident in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
src/hooks/audio/useBackgroundState.ts (1)

18-28: The else if branch conflates two distinct visibility states.

When keepPlayingInBackground is true and the tab goes hidden, the code falls into the else if (isPlaying) branch (line 23) — the same path used for returning to the foreground. Today this is harmless because activeHowl.playing() is true so the guard on line 25 prevents a redundant play() call. However, the intent is unclear and fragile — if a future change alters the Howl lifecycle (e.g., a Howl that isn't playing yet at the moment of hide), this path would call play() on a background-hide event, which is semantically wrong.

Consider making the foreground-return intent explicit:

Suggested improvement
-      if (document.hidden && !keepPlayingInBackground) {
+      if (document.hidden) {
         // When backgrounded, pause audio but maintain isPlaying state
-        if (activeHowl?.playing()) {
+        if (!keepPlayingInBackground && activeHowl?.playing()) {
           activeHowl.pause();
         }
-      } else if (isPlaying) {
+      } else if (!document.hidden && isPlaying) {
         // When returning to foreground, resume from current position
         if (activeHowl && !activeHowl.playing()) {
           activeHowl.play();
         }
       }

This ensures the resume path only triggers on actual foreground transitions.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/hooks/audio/useBackgroundState.ts`:
- Around line 37-50: The effect in useBackgroundState that watches [activeHowl,
isPlaying, keepPlayingInBackground] can cause double-play when a new Howl is
created while the tab is hidden; remove this entire effect block to avoid
calling activeHowl.play() a second time and rely on the existing playback driver
in TTSContext (playAudio → playSentenceWithHowl → howl.play) and the existing
visibilitychange effect to manage pause/resume behavior.
🧹 Nitpick comments (1)
src/hooks/audio/useBackgroundState.ts (1)

13-35: Visibility handler resumes audio unconditionally on foreground return, even when keepPlayingInBackground is true.

When keepPlayingInBackground is true and audio is already playing in the background, the else if (isPlaying) branch (line 23) fires on foreground return and calls activeHowl.play(). While the !activeHowl.playing() guard on line 25 prevents a double-play if the howl is currently playing, there could be a brief window (e.g., between sentences) where the howl is null or not playing, potentially causing unexpected behavior.

This is minor given the guard, but worth noting that the else if on line 23 also matches the case where document.hidden && keepPlayingInBackground — consider making the foreground case explicit:

♻️ More explicit branching
     const handleVisibilityChange = () => {
       setIsBackgrounded(document.hidden);
       if (document.hidden && !keepPlayingInBackground) {
         // When backgrounded, pause audio but maintain isPlaying state
         if (activeHowl?.playing()) {
           activeHowl.pause();
         }
-      } else if (isPlaying) {
+      } else if (!document.hidden && isPlaying) {
         // When returning to foreground, resume from current position
         if (activeHowl && !activeHowl.playing()) {
           activeHowl.play();
         }
       }
     };

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant