diff --git a/App.tsx b/App.tsx index 7b117cf..23e77d4 100644 --- a/App.tsx +++ b/App.tsx @@ -91,6 +91,8 @@ export default function App() { const segmentsToBuffer = article.segments.slice(currentIndex, currentIndex + 5); for (const seg of segmentsToBuffer) { + // Only process if we don't have audio URL, and it's not currently loading. + // This handles cases where we cleared the URL to force regeneration. if (!seg.audioUrl && !seg.isLoading && !seg.hasError) { processSegmentAudio(article.id, seg.id, seg.text, playerState.selectedVoice); } @@ -99,6 +101,38 @@ export default function App() { // -- Handlers -- + const handleVoiceChange = useCallback((newVoice: VoiceName) => { + setPlayerState(prev => ({ ...prev, selectedVoice: newVoice })); + + // Force flush future buffer so new voice is applied immediately + setQueue(prevQueue => prevQueue.map(article => { + // If this is the currently active article + if (article.id === playerState.currentArticleId) { + return { + ...article, + segments: article.segments.map((seg, idx) => { + // Keep the current segment (and past ones) to avoid cutting off mid-speech abruptly + if (idx <= article.currentSegmentIndex) { + return seg; + } + // Invalidate all future segments + return { ...seg, audioUrl: undefined, isLoading: false, hasError: false }; + }) + }; + } + // For inactive articles, invalidate everything + return { + ...article, + segments: article.segments.map(seg => ({ + ...seg, + audioUrl: undefined, + isLoading: false, + hasError: false + })) + }; + })); + }, [playerState.currentArticleId]); + const handleAddUrl = async () => { if (!inputUrl.trim()) return; @@ -373,8 +407,8 @@ export default function App() {
setPlayerState(prev => ({ ...prev, selectedVoice: v }))} - disabled={playerState.isPlaying} + onVoiceChange={handleVoiceChange} + // Removed disabled prop to allow switching while playing />