Add background playback toggle and fix tab-switch audio overlap
This commit is contained in:
parent
64825b626a
commit
d04ab733d2
7 changed files with 75 additions and 11 deletions
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue