@@ -118,10 +118,11 @@ function loadSettings() {
118118
119119function saveSettings ( settings ) {
120120 try {
121- // Store text in IndexedDB, not localStorage
122- const { text, ...rest } = settings ;
121+ // Store text and position in IndexedDB, small settings in localStorage
122+ const { text, currentIndex , ...rest } = settings ;
123123 localStorage . setItem ( STORAGE_KEY , JSON . stringify ( rest ) ) ;
124124 idbSet ( "text" , text ) . catch ( ( ) => { } ) ;
125+ idbSet ( "position" , currentIndex ) . catch ( ( ) => { } ) ;
125126 } catch ( e ) {
126127 console . error ( "Failed to save settings:" , e ) ;
127128 }
@@ -493,6 +494,9 @@ const BookView = memo(function BookView({ words, currentIndex, sideOpacity, setC
493494 ) ;
494495} ) ;
495496
497+ // Capture URL hash before any effects can modify it
498+ const initialUrlHash = window . location . hash ;
499+
496500function App ( ) {
497501 // Load settings only once on mount
498502 const [ savedSettings ] = useState ( ( ) => loadSettings ( ) ) ;
@@ -534,31 +538,26 @@ function App() {
534538 return clamp ( getPositionForText ( savedSettings ?. text || DEFAULT_TEXT , savedSettings ?. positions || { } ) ) ;
535539 } ) ;
536540
537- // Load text from IndexedDB on mount (async, overrides default text)
541+ // Load text and position from IndexedDB on mount
538542 const [ idbLoaded , setIdbLoaded ] = useState ( false ) ;
539543 useEffect ( ( ) => {
540- idbGet ( "text" ) . then ( ( savedText ) => {
544+ Promise . all ( [ idbGet ( "text" ) , idbGet ( "position" ) ] ) . then ( ( [ savedText , savedPos ] ) => {
541545 if ( savedText && savedText !== DEFAULT_TEXT ) {
542546 setText ( savedText ) ;
543547 const parsed = parseText ( savedText ) ;
544548 setWords ( parsed . words ) ;
545549 setParagraphBreaks ( parsed . breaks ) ;
546- // Restore position for this text
547- const hash = window . location . hash ;
550+ // Restore position: URL hash (captured before effects) > IndexedDB > 0
548551 let pos = 0 ;
549- if ( hash ) {
550- const params = new URLSearchParams ( hash . slice ( 1 ) ) ;
552+ if ( initialUrlHash ) {
553+ const params = new URLSearchParams ( initialUrlHash . slice ( 1 ) ) ;
551554 const urlPos = parseInt ( params . get ( "pos" ) , 10 ) ;
552555 if ( ! isNaN ( urlPos ) && urlPos >= 0 ) pos = urlPos ;
553- } else {
554- try {
555- const stored = localStorage . getItem ( "rsvp-current-index" ) ;
556- if ( stored != null ) pos = parseInt ( stored , 10 ) || 0 ;
557- } catch { }
556+ } else if ( typeof savedPos === "number" ) {
557+ pos = savedPos ;
558558 }
559559 const clamped = Math . min ( Math . max ( 0 , pos ) , Math . max ( 0 , parsed . words . length - 1 ) ) ;
560560 _setCurrentIndex ( clamped ) ;
561- try { localStorage . setItem ( "rsvp-current-index" , String ( clamped ) ) ; } catch { }
562561 }
563562 setIdbLoaded ( true ) ;
564563 } ) . catch ( ( ) => setIdbLoaded ( true ) ) ;
@@ -669,35 +668,36 @@ function App() {
669668
670669
671670
672- // Save settings including position for current text
673671 // Save position for current text (lightweight, every word change)
674672 useEffect ( ( ) => {
673+ if ( ! idbLoaded ) return ;
675674 positionsRef . current = savePositionForText (
676675 text ,
677676 currentIndex ,
678677 positionsRef . current ,
679678 ) ;
680679 try { localStorage . setItem ( "rsvp-current-index" , String ( currentIndex ) ) ; } catch { }
681- } , [ text , currentIndex ] ) ;
680+ } , [ text , currentIndex , idbLoaded ] ) ;
682681
683682 // Save full settings when settings/text change or playback stops
684683 useEffect ( ( ) => {
685- if ( ! isPlaying ) {
686- saveSettings ( {
687- wpm,
688- text,
689- currentIndex,
690- positions : positionsRef . current ,
691- sideOpacity,
692- bookView,
693- bookMetadata,
694- fetchMetadataOnline,
695- } ) ;
696- }
697- } , [ wpm , text , isPlaying , sideOpacity , bookView , bookMetadata , fetchMetadataOnline ] ) ;
684+ if ( ! idbLoaded || isPlaying ) return ;
685+ saveSettings ( {
686+ wpm,
687+ text,
688+ currentIndex,
689+ positions : positionsRef . current ,
690+ sideOpacity,
691+ bookView,
692+ bookMetadata,
693+ fetchMetadataOnline,
694+ } ) ;
695+ } , [ wpm , text , isPlaying , sideOpacity , bookView , bookMetadata , fetchMetadataOnline , idbLoaded ] ) ;
698696
697+ // Save full settings when page unloads or goes to background (iOS)
699698 // Save full settings when page unloads or goes to background (iOS)
700699 useEffect ( ( ) => {
700+ if ( ! idbLoaded ) return ;
701701 const save = ( ) => {
702702 saveSettings ( {
703703 wpm,
@@ -719,14 +719,14 @@ function App() {
719719 window . removeEventListener ( "beforeunload" , save ) ;
720720 document . removeEventListener ( "visibilitychange" , handleVisibility ) ;
721721 } ;
722- } , [ wpm , text , currentIndex , sideOpacity , bookView , bookMetadata , fetchMetadataOnline ] ) ;
722+ } , [ wpm , text , currentIndex , sideOpacity , bookView , bookMetadata , fetchMetadataOnline , idbLoaded ] ) ;
723723
724724 // Update URL hash with current position in real time
725725 useEffect ( ( ) => {
726- if ( words . length > 0 ) {
726+ if ( idbLoaded && words . length > 0 ) {
727727 window . history . replaceState ( null , "" , `${ window . location . pathname } #pos=${ currentIndex } ` ) ;
728728 }
729- } , [ currentIndex , words . length ] ) ;
729+ } , [ currentIndex , words . length , idbLoaded ] ) ;
730730
731731 const getBaseDelay = useCallback ( ( ) => {
732732 return ( 60 / wpm ) * 1000 ;
0 commit comments