Add background playback toggle and fix tab-switch audio overlap

This commit is contained in:
ru-aish 2026-02-11 17:15:16 +05:30
parent 64825b626a
commit d04ab733d2
7 changed files with 75 additions and 11 deletions

View file

@ -28,6 +28,7 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html }: {
const {
viewType,
skipBlank,
keepPlayingInBackground,
epubTheme,
smartSentenceSplitting,
headerMargin,
@ -316,6 +317,20 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html }: {
Automatically skip pages with no text content
</p>
</div>}
{!html && <div className="space-y-1">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={keepPlayingInBackground}
onChange={(e) => updateConfigKey('keepPlayingInBackground', e.target.checked)}
className="form-checkbox h-4 w-4 text-accent rounded border-muted"
/>
<span className="text-sm font-medium text-foreground">Keep playing in background</span>
</label>
<p className="text-sm text-muted pl-6">
Continue TTS playback when this tab is not active
</p>
</div>}
{!html && (
<div className="space-y-1">
<label className="flex items-center space-x-2">

View file

@ -43,7 +43,16 @@ export function SettingsModal() {
const [isOpen, setIsOpen] = useState(false);
const { theme, setTheme } = useTheme();
const { apiKey, baseUrl, ttsProvider, ttsModel, ttsInstructions, updateConfig, updateConfigKey } = useConfig();
const {
apiKey,
baseUrl,
ttsProvider,
ttsModel,
ttsInstructions,
keepPlayingInBackground,
updateConfig,
updateConfigKey
} = useConfig();
const { clearPDFs, clearEPUBs, clearHTML } = useDocuments();
const [localApiKey, setLocalApiKey] = useState(apiKey);
const [localBaseUrl, setLocalBaseUrl] = useState(baseUrl);
@ -674,6 +683,21 @@ export function SettingsModal() {
</TabPanel>
<TabPanel className="space-y-4">
<div className="space-y-1">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={keepPlayingInBackground}
onChange={(e) => updateConfigKey('keepPlayingInBackground', e.target.checked)}
className="form-checkbox h-4 w-4 text-accent rounded border-muted"
/>
<span className="text-sm font-medium text-foreground">Keep playing in background</span>
</label>
<p className="text-sm text-muted pl-6">
Continue TTS playback when this tab is not active
</p>
</div>
<div className="space-y-1">
<label className="block text-sm font-medium text-foreground">Server Library Import</label>
<div className="flex gap-2">

View file

@ -18,6 +18,7 @@ interface ConfigContextType {
audioPlayerSpeed: number;
voice: string;
skipBlank: boolean;
keepPlayingInBackground: boolean;
epubTheme: boolean;
smartSentenceSplitting: boolean;
headerMargin: number;
@ -162,6 +163,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
audioPlayerSpeed,
voice,
skipBlank,
keepPlayingInBackground,
epubTheme,
headerMargin,
footerMargin,
@ -258,6 +260,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
audioPlayerSpeed,
voice,
skipBlank,
keepPlayingInBackground,
epubTheme,
smartSentenceSplitting,
headerMargin,

View file

@ -285,6 +285,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
ttsProvider: configTTSProvider,
ttsModel: configTTSModel,
ttsInstructions: configTTSInstructions,
keepPlayingInBackground,
updateConfigKey,
skipBlank,
smartSentenceSplitting,
@ -1141,7 +1142,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
const isBackgrounded = useBackgroundState({
activeHowl,
isPlaying,
playAudio,
keepPlayingInBackground,
});
// Track the current word index during playback using Howler's seek position
@ -1221,7 +1222,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
if (isProcessing) return; // Don't proceed if processing audio
if (!sentences[currentIndex]) return; // Don't proceed if no sentence to play
if (activeHowl) return; // Don't proceed if audio is already playing
if (isBackgrounded) return; // Don't proceed if backgrounded
if (isBackgrounded && !keepPlayingInBackground) return; // Don't proceed if background playback is disabled
// Start playing current sentence
playAudio();
@ -1242,6 +1243,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
sentences,
activeHowl,
isBackgrounded,
keepPlayingInBackground,
playAudio,
preloadNextAudio,
abortAudio

View file

@ -4,23 +4,25 @@ import { Howl } from 'howler';
interface UseBackgroundStateProps {
activeHowl: Howl | null;
isPlaying: boolean;
playAudio: () => void;
keepPlayingInBackground: boolean;
}
export function useBackgroundState({ activeHowl, isPlaying, playAudio }: UseBackgroundStateProps) {
export function useBackgroundState({ activeHowl, isPlaying, keepPlayingInBackground }: UseBackgroundStateProps) {
const [isBackgrounded, setIsBackgrounded] = useState(false);
useEffect(() => {
setIsBackgrounded(document.hidden);
const handleVisibilityChange = () => {
setIsBackgrounded(document.hidden);
if (document.hidden) {
if (document.hidden && !keepPlayingInBackground) {
// When backgrounded, pause audio but maintain isPlaying state
if (activeHowl) {
if (activeHowl?.playing()) {
activeHowl.pause();
}
} else if (isPlaying) {
// When returning to foreground, resume from current position
if (activeHowl) {
if (activeHowl && !activeHowl.playing()) {
activeHowl.play();
}
}
@ -30,7 +32,22 @@ export function useBackgroundState({ activeHowl, isPlaying, playAudio }: UseBack
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [isPlaying, activeHowl, playAudio]);
}, [isPlaying, activeHowl, keepPlayingInBackground]);
useEffect(() => {
if (!document.hidden || !activeHowl || !isPlaying) return;
if (keepPlayingInBackground) {
if (!activeHowl.playing()) {
activeHowl.play();
}
return;
}
if (activeHowl.playing()) {
activeHowl.pause();
}
}, [activeHowl, isPlaying, keepPlayingInBackground]);
return isBackgrounded;
}
}

View file

@ -156,6 +156,8 @@ function buildAppConfigFromRaw(raw: RawConfigMap): AppConfigRow {
audioPlayerSpeed: raw.audioPlayerSpeed ? parseFloat(raw.audioPlayerSpeed) : APP_CONFIG_DEFAULTS.audioPlayerSpeed,
voice: '',
skipBlank: raw.skipBlank === 'false' ? false : APP_CONFIG_DEFAULTS.skipBlank,
keepPlayingInBackground:
raw.keepPlayingInBackground === 'false' ? false : APP_CONFIG_DEFAULTS.keepPlayingInBackground,
epubTheme: raw.epubTheme === 'true',
smartSentenceSplitting:
raw.smartSentenceSplitting === 'false' ? false : APP_CONFIG_DEFAULTS.smartSentenceSplitting,
@ -1008,4 +1010,3 @@ export async function importDocumentsFromLibrary(
}
}

View file

@ -14,6 +14,7 @@ export interface AppConfigValues {
audioPlayerSpeed: number;
voice: string;
skipBlank: boolean;
keepPlayingInBackground: boolean;
epubTheme: boolean;
headerMargin: number;
footerMargin: number;
@ -40,6 +41,7 @@ export const APP_CONFIG_DEFAULTS: AppConfigValues = {
audioPlayerSpeed: 1,
voice: '',
skipBlank: true,
keepPlayingInBackground: true,
epubTheme: false,
headerMargin: 0,
footerMargin: 0,