From 966a58413a58185abc226fffa6f8d670e038b6b6 Mon Sep 17 00:00:00 2001 From: gkxk Date: Tue, 20 Oct 2026 11:55:49 +0800 Subject: [PATCH] fix(persistence): strengthen state persistence, clean novel intro, and adjust HUD text --- public/books/The Well of Ascension.txt | 2 - src/App.tsx | 66 ++++++++++++++++++-------- src/hooks/useReaderEngine.ts | 34 +++++++++++-- 3 files changed, 76 insertions(+), 26 deletions(-) diff --git a/public/books/The Well of Ascension.txt b/public/books/The Well of Ascension.txt index bda6994..2dc1bfe 100755 --- a/public/books/The Well of Ascension.txt +++ b/public/books/The Well of Ascension.txt @@ -1,5 +1,3 @@ -=== Read The Well of Ascension online free by Brandon Sanderson - Novel80 === - PART ONE diff --git a/src/App.tsx b/src/App.tsx index 11bd72c..cb7b954 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -62,35 +62,54 @@ export default function App() { saveState({ fontSize: newSize }); }; - // Throttled persistence of wordIndex to localStorage + // Robust state persistence const lastSavedIndexRef = useRef(saved.wordIndex); - const saveTimeoutRef = useRef(null); + const fpmRef = useRef(flashesPerMinute); + const chunkRef = useRef(wordsPerChunk); + const fontSizeRef = useRef(fontSize); - const handleWordIndexUpdate = useCallback((index: number) => { - if (saveTimeoutRef.current) { - window.clearTimeout(saveTimeoutRef.current); - } - saveTimeoutRef.current = window.setTimeout(() => { - if (index !== lastSavedIndexRef.current) { - lastSavedIndexRef.current = index; - saveState({ wordIndex: index }); - } - }, 400); + useEffect(() => { + fpmRef.current = flashesPerMinute; + }, [flashesPerMinute]); + + useEffect(() => { + chunkRef.current = wordsPerChunk; + }, [wordsPerChunk]); + + useEffect(() => { + fontSizeRef.current = fontSize; + }, [fontSize]); + + const persistCurrentState = useCallback((targetIndex?: number) => { + const idx = + typeof targetIndex === 'number' + ? targetIndex + : lastSavedIndexRef.current; + lastSavedIndexRef.current = idx; + saveState({ + wordIndex: idx, + flashesPerMinute: fpmRef.current, + wordsPerChunk: chunkRef.current, + fontSize: fontSizeRef.current, + }); }, []); + // Autosave periodically every 3 seconds + useEffect(() => { + const interval = window.setInterval(() => { + persistCurrentState(lastSavedIndexRef.current); + }, 3000); + return () => window.clearInterval(interval); + }, [persistCurrentState]); + // Save immediately on unload useEffect(() => { const handleBeforeUnload = () => { - saveState({ - wordIndex: lastSavedIndexRef.current, - flashesPerMinute, - wordsPerChunk, - fontSize, - }); + persistCurrentState(lastSavedIndexRef.current); }; window.addEventListener('beforeunload', handleBeforeUnload); return () => window.removeEventListener('beforeunload', handleBeforeUnload); - }, [flashesPerMinute, wordsPerChunk, fontSize]); + }, [persistCurrentState]); // Core reading engine const { @@ -110,7 +129,12 @@ export default function App() { initialWordIndex: saved.wordIndex, onWordIndexChange: (idx) => { lastSavedIndexRef.current = idx; - handleWordIndexUpdate(idx); + }, + onPause: (idx) => { + persistCurrentState(idx); + }, + onJump: (idx) => { + persistCurrentState(idx); }, }); @@ -268,7 +292,7 @@ export default function App() { {wpm} WPM{' '} - ({flashesPerMinute} FPM × {wordsPerChunk}) + ({flashesPerMinute} FPM × {wordsPerChunk} 词) diff --git a/src/hooks/useReaderEngine.ts b/src/hooks/useReaderEngine.ts index a111f10..3f39bde 100644 --- a/src/hooks/useReaderEngine.ts +++ b/src/hooks/useReaderEngine.ts @@ -7,6 +7,8 @@ interface UseReaderEngineProps { wordsPerChunk: number; initialWordIndex: number; onWordIndexChange?: (index: number) => void; + onPause?: (index: number) => void; + onJump?: (index: number) => void; } export function useReaderEngine({ @@ -15,6 +17,8 @@ export function useReaderEngine({ wordsPerChunk, initialWordIndex, onWordIndexChange, + onPause, + onJump, }: UseReaderEngineProps) { const [currentIndex, setCurrentIndex] = useState(() => Math.max(0, initialWordIndex || 0) @@ -51,6 +55,23 @@ export function useReaderEngine({ const expectedTickTimeRef = useRef(null); const scheduleNextRef = useRef<((fromIdx: number) => void) | null>(null); + // Refs for callbacks + const onWordIndexChangeRef = useRef(onWordIndexChange); + const onPauseRef = useRef(onPause); + const onJumpRef = useRef(onJump); + + useEffect(() => { + onWordIndexChangeRef.current = onWordIndexChange; + }, [onWordIndexChange]); + + useEffect(() => { + onPauseRef.current = onPause; + }, [onPause]); + + useEffect(() => { + onJumpRef.current = onJump; + }, [onJump]); + // Keep refs synchronized useEffect(() => { wordsRef.current = words; @@ -66,8 +87,8 @@ export function useReaderEngine({ useEffect(() => { currentIndexRef.current = currentIndex; - onWordIndexChange?.(currentIndex); - }, [currentIndex, onWordIndexChange]); + onWordIndexChangeRef.current?.(currentIndex); + }, [currentIndex]); useEffect(() => { isPlayingRef.current = isPlaying; @@ -93,6 +114,7 @@ export function useReaderEngine({ setIsPlaying(false); isPlayingRef.current = false; expectedTickTimeRef.current = null; + onPauseRef.current?.(fromIndex); return; } @@ -123,7 +145,10 @@ export function useReaderEngine({ setIsPlaying(false); isPlayingRef.current = false; expectedTickTimeRef.current = null; - setCurrentIndex(currentWords.length - 1); + const finalIndex = currentWords.length - 1; + setCurrentIndex(finalIndex); + currentIndexRef.current = finalIndex; + onPauseRef.current?.(finalIndex); return; } @@ -157,6 +182,7 @@ export function useReaderEngine({ expectedTickTimeRef.current = null; setIsPlaying(false); isPlayingRef.current = false; + onPauseRef.current?.(currentIndexRef.current); }, []); const togglePlay = useCallback(() => { @@ -174,6 +200,7 @@ export function useReaderEngine({ setCurrentIndex(target); currentIndexRef.current = target; expectedTickTimeRef.current = null; + onJumpRef.current?.(target); if (isPlayingRef.current) { if (timerRef.current) { @@ -223,6 +250,7 @@ export function useReaderEngine({ pause, togglePlay, jumpTo, + jumpToIndex: jumpTo, jumpBy, restart, totalWords: words.length,