@@ -94,23 +94,13 @@ export function TTSScreen() {
9494 const theme = getTheme ( actualTheme === "dark" ) ;
9595 const ttsStore = useTTSStore ( ) ;
9696
97- // ── Sync state from store when expanding from mini player ──────────
98- useEffect ( ( ) => {
99- if ( ttsStore . currentBookId === book . id && ttsStore . paragraphs . length > 0 ) {
100- setParagraphs ( ttsStore . paragraphs ) ;
101- setChapterTitle ( ttsStore . chapterTitle ) ;
102- setChapterIndex ( ttsStore . chapterIndex ) ;
103- setCurrentParagraph ( ttsStore . currentParagraph ) ;
104- if ( ttsStore . selectedProvider ) setProvider ( ttsStore . selectedProvider ) ;
105- if ( ttsStore . selectedVoice ?. id ) setVoiceId ( ttsStore . selectedVoice . id ) ;
106- setLoading ( false ) ;
107- }
108- } , [ ] ) ;
97+ // Derived state from store (single source of truth)
98+ const paragraphs = ttsStore . paragraphs ;
99+ const chapterTitle = ttsStore . chapterTitle ;
100+ const chapterIndex = ttsStore . chapterIndex ;
101+ const currentParagraph = ttsStore . currentParagraph ;
109102
110- // ── Book + chapter state ─────────────────────────────────────────────
111- const [ paragraphs , setParagraphs ] = useState < Paragraph [ ] > ( [ ] ) ;
112- const [ chapterTitle , setChapterTitle ] = useState ( "" ) ;
113- const [ chapterIndex , setChapterIndex ] = useState ( 0 ) ;
103+ // ── Local UI state ──────────────────────────────────────────────────
114104 const [ chapters , setChapters ] = useState < { title : string ; index : number } [ ] > (
115105 [ ] ,
116106 ) ;
@@ -146,7 +136,6 @@ export function TTSScreen() {
146136 const isPaused = ttsStore . state === 'paused' ;
147137 const isLoadingAudio = ttsStore . state === 'loading' || playbackState === State . Connecting || playbackState === State . Buffering ;
148138
149- const [ currentParagraph , setCurrentParagraph ] = useState ( 0 ) ;
150139 const [ paragraphProgress , setParagraphProgress ] = useState ( 0 ) ;
151140 const [ resumeOffsetMs , setResumeOffsetMs ] = useState ( 0 ) ;
152141
@@ -185,8 +174,7 @@ export function TTSScreen() {
185174 progressUpdateEventInterval : 1 ,
186175 } ) ;
187176 // Only reset if nothing is playing (avoid interrupting playback from mini player)
188- const state = await TrackPlayer . getPlaybackState ( ) ;
189- if ( state . state !== State . Playing && state . state !== State . Buffering ) {
177+ if ( ttsStore . state !== 'playing' ) {
190178 await TrackPlayer . reset ( ) ;
191179 }
192180 } catch ( e ) {
@@ -345,47 +333,6 @@ export function TTSScreen() {
345333 return ( ) => sub . remove ( ) ;
346334 } , [ currentParagraph , paragraphs . length , chapterIndex , voiceId , provider , ttsStore ] ) ;
347335
348- // ── TrackPlayer event: active track changed → update current paragraph ─
349- useEffect ( ( ) => {
350- const sub = TrackPlayer . addEventListener ( Event . PlaybackActiveTrackChanged , async ( event ) => {
351- if ( event . index === undefined ) return ;
352-
353- // Get current track to determine which paragraph we're on
354- const track = await TrackPlayer . getTrack ( event . index ) ;
355- if ( ! track ?. id ) return ;
356-
357- // Extract paragraph index from track ID (format: "paraId-chunkIdx")
358- const paraId = track . id . split ( '-' ) [ 0 ] ;
359- const paraIdx = paragraphs . findIndex ( p => p . id === paraId ) ;
360-
361- if ( paraIdx >= 0 && paraIdx !== currentParagraph ) {
362- setCurrentParagraph ( paraIdx ) ;
363- ttsStore . setCurrentParagraph ( paraIdx ) ;
364-
365- // Add next paragraph to queue for seamless playback
366- try {
367- const nextIdx = paraIdx + 1 ;
368- if ( nextIdx < paragraphs . length ) {
369- const nextUris = await synthesizeParagraph ( nextIdx ) ;
370- if ( nextUris . length > 0 ) {
371- const nextTracks = nextUris . map ( ( uri , i ) => ( {
372- id : `${ paragraphs [ nextIdx ] . id } -${ i } ` ,
373- url : uri ,
374- title : `${ book . title || '未知书籍' } - ${ chapterTitle } ` ,
375- artist : book . author || "未知作者" ,
376- artwork : undefined ,
377- duration : 0 ,
378- } ) ) ;
379- await TrackPlayer . add ( nextTracks ) ;
380- }
381- }
382- } catch {
383- // Ignore prefetch errors
384- }
385- }
386- } ) ;
387- return ( ) => sub . remove ( ) ;
388- } , [ ttsStore , paragraphs , currentParagraph , book , chapterTitle , synthesizeParagraph ] ) ;
389336
390337 // Auto-scroll to current paragraph in content view
391338 useEffect ( ( ) => {
@@ -405,7 +352,7 @@ export function TTSScreen() {
405352
406353 const loadChapter = async ( ci : number , vid : string , prov : string ) => {
407354 console . log ( '[TTSScreen] loadChapter called, ci:' , ci , 'vid:' , vid , 'prov:' , prov ) ;
408- setChapterIndex ( ci ) ;
355+ ttsStore . setChapterIndex ( ci ) ;
409356 try {
410357 const apiClient = getApiClient ( ) ;
411358 console . log ( '[TTSScreen] Fetching paragraphs for book:' , book . id , 'chapter:' , ci ) ;
@@ -430,16 +377,14 @@ export function TTSScreen() {
430377 }
431378 }
432379
433- setChapterTitle ( pRes . data . title ) ;
434- setParagraphs ( pRes . data . paragraphs ) ;
435- setCurrentParagraph ( 0 ) ;
436380 setParagraphProgress ( 0 ) ;
437381 setResumeOffsetMs ( 0 ) ;
438382 prefetchedRef . current . clear ( ) ;
439383 ttsStore . setParagraphs ( pRes . data . paragraphs ) ;
440384 ttsStore . setTotalParagraphs ( pRes . data . paragraphs . length ) ;
441385 ttsStore . setChapterTitle ( pRes . data . title ) ;
442386 ttsStore . setChapterIndex ( ci ) ;
387+ ttsStore . setCurrentParagraph ( 0 ) ;
443388
444389 // Resume from saved cloud progress
445390 try {
@@ -452,7 +397,7 @@ export function TTSScreen() {
452397 }
453398 if ( rec . voice ) setVoiceId ( rec . voice ) ;
454399 setResumeOffsetMs ( Math . max ( 0 , rec . audioOffsetMs || 0 ) ) ;
455- setCurrentParagraph (
400+ ttsStore . setCurrentParagraph (
456401 Math . min ( rec . paragraphIndex , pRes . data . paragraphs . length - 1 ) ,
457402 ) ;
458403 }
@@ -506,27 +451,21 @@ export function TTSScreen() {
506451 // ── Persist latest position when user leaves the screen ─────────────
507452 useEffect ( ( ) => {
508453 const unsubscribe = navigation . addListener ( "beforeRemove" , async ( ) => {
509- await persistProgress ( currentParagraph , Math . round ( progress . position * 1000 ) ) ;
510- // Sync state to store
511- ttsStore . setParagraphs ( paragraphs ) ;
512- ttsStore . setTotalParagraphs ( paragraphs . length ) ;
513- ttsStore . setChapterTitle ( chapterTitle ) ;
514- ttsStore . setChapterIndex ( chapterIndex ) ;
515- ttsStore . setCurrentParagraph ( currentParagraph ) ;
454+ await persistProgress ( ttsStore . currentParagraph , Math . round ( progress . position * 1000 ) ) ;
516455 ttsStore . setMiniPlayerVisible ( true ) ;
517456 } ) ;
518457 return unsubscribe ;
519- } , [ currentParagraph , navigation , persistProgress , progress . position , ttsStore , paragraphs , chapterTitle , chapterIndex ] ) ;
458+ } , [ navigation , persistProgress , progress . position , ttsStore ] ) ;
520459
521460 // ── Persist progress when app goes to background ────────────────────
522461 useEffect ( ( ) => {
523462 const subscription = AppState . addEventListener ( "change" , ( nextAppState ) => {
524463 if ( nextAppState === "background" || nextAppState === "inactive" ) {
525- void persistProgress ( currentParagraph , Math . round ( progress . position * 1000 ) ) ;
464+ void persistProgress ( ttsStore . currentParagraph , Math . round ( progress . position * 1000 ) ) ;
526465 }
527466 } ) ;
528467 return ( ) => subscription . remove ( ) ;
529- } , [ currentParagraph , persistProgress , progress . position ] ) ;
468+ } , [ persistProgress , progress . position , ttsStore ] ) ;
530469
531470 const synthesizeParagraph = useCallback (
532471 async ( idx : number ) : Promise < string [ ] > => {
@@ -559,17 +498,57 @@ export function TTSScreen() {
559498 [ book . id , paragraphs , provider , voiceId ] ,
560499 ) ;
561500
501+ // ── TrackPlayer event: active track changed → update current paragraph ─
502+ useEffect ( ( ) => {
503+ const sub = TrackPlayer . addEventListener ( Event . PlaybackTrackChanged , async ( event : any ) => {
504+ if ( event . nextTrack === undefined ) return ;
505+
506+ // Get current track to determine which paragraph we're on
507+ const track = await TrackPlayer . getTrack ( event . nextTrack ) ;
508+ if ( ! track ?. id ) return ;
509+
510+ // Extract paragraph index from track ID (format: "paraId-chunkIdx")
511+ const paraId = track . id . split ( '-' ) [ 0 ] ;
512+ const paraIdx = paragraphs . findIndex ( p => p . id === paraId ) ;
513+
514+ if ( paraIdx >= 0 && paraIdx !== ttsStore . currentParagraph ) {
515+ ttsStore . setCurrentParagraph ( paraIdx ) ;
516+
517+ // Add next paragraph to queue for seamless playback
518+ try {
519+ const nextIdx = paraIdx + 1 ;
520+ if ( nextIdx < paragraphs . length ) {
521+ const nextUris = await synthesizeParagraph ( nextIdx ) ;
522+ if ( nextUris . length > 0 ) {
523+ const nextTracks = nextUris . map ( ( uri , i ) => ( {
524+ id : `${ paragraphs [ nextIdx ] . id } -${ i } ` ,
525+ url : uri ,
526+ title : `${ book . title || '未知书籍' } - ${ chapterTitle } ` ,
527+ artist : book . author || "未知作者" ,
528+ artwork : undefined ,
529+ duration : 0 ,
530+ } ) ) ;
531+ await TrackPlayer . add ( nextTracks ) ;
532+ }
533+ }
534+ } catch {
535+ // Ignore prefetch errors
536+ }
537+ }
538+ } ) ;
539+ return ( ) => sub . remove ( ) ;
540+ } , [ ttsStore , paragraphs , book , chapterTitle , synthesizeParagraph ] ) ;
541+
562542 const playParagraph = useCallback (
563543 async ( idx : number ) => {
564544 if ( idx >= paragraphs . length ) {
565- setCurrentParagraph ( 0 ) ;
545+ ttsStore . setCurrentParagraph ( 0 ) ;
566546 setParagraphProgress ( 0 ) ;
567547 ttsStore . setState ( "idle" ) ;
568548 return ;
569549 }
570- setCurrentParagraph ( idx ) ;
571- setParagraphProgress ( 0 ) ;
572550 ttsStore . setCurrentParagraph ( idx ) ;
551+ setParagraphProgress ( 0 ) ;
573552 const startOffsetMs = resumeOffsetMs ;
574553 setResumeOffsetMs ( 0 ) ;
575554
@@ -658,7 +637,7 @@ export function TTSScreen() {
658637 const handleStop = useCallback ( async ( ) => {
659638 await TrackPlayer . reset ( ) ;
660639 ttsStore . setState ( "idle" ) ;
661- setCurrentParagraph ( 0 ) ;
640+ ttsStore . setCurrentParagraph ( 0 ) ;
662641 setParagraphProgress ( 0 ) ;
663642 } , [ ttsStore ] ) ;
664643
@@ -668,7 +647,6 @@ export function TTSScreen() {
668647 if ( isPlaying || isPaused ) {
669648 await playParagraph ( idx ) ;
670649 } else {
671- setCurrentParagraph ( idx ) ;
672650 ttsStore . setCurrentParagraph ( idx ) ;
673651 void persistProgress ( idx ) ;
674652 }
@@ -740,15 +718,9 @@ export function TTSScreen() {
740718 } , [ ] ) ;
741719
742720 const handleMinimize = useCallback ( ( ) => {
743- // Sync current state to store before minimizing
744- ttsStore . setParagraphs ( paragraphs ) ;
745- ttsStore . setTotalParagraphs ( paragraphs . length ) ;
746- ttsStore . setChapterTitle ( chapterTitle ) ;
747- ttsStore . setChapterIndex ( chapterIndex ) ;
748- ttsStore . setCurrentParagraph ( currentParagraph ) ;
749721 ttsStore . setMiniPlayerVisible ( true ) ;
750722 navigation . goBack ( ) ;
751- } , [ ttsStore , navigation , paragraphs , chapterTitle , chapterIndex , currentParagraph ] ) ;
723+ } , [ ttsStore , navigation ] ) ;
752724
753725 // ── Progress calculation ─────────────────────────────────────────────
754726 const overallProgress = paragraphs . length
0 commit comments