fix(persistence): strengthen state persistence, clean novel intro, and adjust HUD text

This commit is contained in:
2026-10-20 11:55:49 +08:00
parent 14f1956bbf
commit 966a58413a
3 changed files with 76 additions and 26 deletions

View File

@@ -1,5 +1,3 @@
=== Read The Well of Ascension online free by Brandon Sanderson - Novel80 ===
PART ONE PART ONE

View File

@@ -62,35 +62,54 @@ export default function App() {
saveState({ fontSize: newSize }); saveState({ fontSize: newSize });
}; };
// Throttled persistence of wordIndex to localStorage // Robust state persistence
const lastSavedIndexRef = useRef<number>(saved.wordIndex); const lastSavedIndexRef = useRef<number>(saved.wordIndex);
const saveTimeoutRef = useRef<number | null>(null); const fpmRef = useRef<number>(flashesPerMinute);
const chunkRef = useRef<number>(wordsPerChunk);
const fontSizeRef = useRef<FontSize>(fontSize);
const handleWordIndexUpdate = useCallback((index: number) => { useEffect(() => {
if (saveTimeoutRef.current) { fpmRef.current = flashesPerMinute;
window.clearTimeout(saveTimeoutRef.current); }, [flashesPerMinute]);
}
saveTimeoutRef.current = window.setTimeout(() => { useEffect(() => {
if (index !== lastSavedIndexRef.current) { chunkRef.current = wordsPerChunk;
lastSavedIndexRef.current = index; }, [wordsPerChunk]);
saveState({ wordIndex: index });
} useEffect(() => {
}, 400); 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 // Save immediately on unload
useEffect(() => { useEffect(() => {
const handleBeforeUnload = () => { const handleBeforeUnload = () => {
saveState({ persistCurrentState(lastSavedIndexRef.current);
wordIndex: lastSavedIndexRef.current,
flashesPerMinute,
wordsPerChunk,
fontSize,
});
}; };
window.addEventListener('beforeunload', handleBeforeUnload); window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload); return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [flashesPerMinute, wordsPerChunk, fontSize]); }, [persistCurrentState]);
// Core reading engine // Core reading engine
const { const {
@@ -110,7 +129,12 @@ export default function App() {
initialWordIndex: saved.wordIndex, initialWordIndex: saved.wordIndex,
onWordIndexChange: (idx) => { onWordIndexChange: (idx) => {
lastSavedIndexRef.current = idx; lastSavedIndexRef.current = idx;
handleWordIndexUpdate(idx); },
onPause: (idx) => {
persistCurrentState(idx);
},
onJump: (idx) => {
persistCurrentState(idx);
}, },
}); });
@@ -268,7 +292,7 @@ export default function App() {
<span className="text-zinc-400 font-semibold"> <span className="text-zinc-400 font-semibold">
{wpm} WPM{' '} {wpm} WPM{' '}
<span className="font-normal text-zinc-500 text-[10px]"> <span className="font-normal text-zinc-500 text-[10px]">
({flashesPerMinute} FPM × {wordsPerChunk}) ({flashesPerMinute} FPM × {wordsPerChunk} 词)
</span> </span>
</span> </span>
</div> </div>

View File

@@ -7,6 +7,8 @@ interface UseReaderEngineProps {
wordsPerChunk: number; wordsPerChunk: number;
initialWordIndex: number; initialWordIndex: number;
onWordIndexChange?: (index: number) => void; onWordIndexChange?: (index: number) => void;
onPause?: (index: number) => void;
onJump?: (index: number) => void;
} }
export function useReaderEngine({ export function useReaderEngine({
@@ -15,6 +17,8 @@ export function useReaderEngine({
wordsPerChunk, wordsPerChunk,
initialWordIndex, initialWordIndex,
onWordIndexChange, onWordIndexChange,
onPause,
onJump,
}: UseReaderEngineProps) { }: UseReaderEngineProps) {
const [currentIndex, setCurrentIndex] = useState<number>(() => const [currentIndex, setCurrentIndex] = useState<number>(() =>
Math.max(0, initialWordIndex || 0) Math.max(0, initialWordIndex || 0)
@@ -51,6 +55,23 @@ export function useReaderEngine({
const expectedTickTimeRef = useRef<number | null>(null); const expectedTickTimeRef = useRef<number | null>(null);
const scheduleNextRef = useRef<((fromIdx: number) => void) | null>(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 // Keep refs synchronized
useEffect(() => { useEffect(() => {
wordsRef.current = words; wordsRef.current = words;
@@ -66,8 +87,8 @@ export function useReaderEngine({
useEffect(() => { useEffect(() => {
currentIndexRef.current = currentIndex; currentIndexRef.current = currentIndex;
onWordIndexChange?.(currentIndex); onWordIndexChangeRef.current?.(currentIndex);
}, [currentIndex, onWordIndexChange]); }, [currentIndex]);
useEffect(() => { useEffect(() => {
isPlayingRef.current = isPlaying; isPlayingRef.current = isPlaying;
@@ -93,6 +114,7 @@ export function useReaderEngine({
setIsPlaying(false); setIsPlaying(false);
isPlayingRef.current = false; isPlayingRef.current = false;
expectedTickTimeRef.current = null; expectedTickTimeRef.current = null;
onPauseRef.current?.(fromIndex);
return; return;
} }
@@ -123,7 +145,10 @@ export function useReaderEngine({
setIsPlaying(false); setIsPlaying(false);
isPlayingRef.current = false; isPlayingRef.current = false;
expectedTickTimeRef.current = null; expectedTickTimeRef.current = null;
setCurrentIndex(currentWords.length - 1); const finalIndex = currentWords.length - 1;
setCurrentIndex(finalIndex);
currentIndexRef.current = finalIndex;
onPauseRef.current?.(finalIndex);
return; return;
} }
@@ -157,6 +182,7 @@ export function useReaderEngine({
expectedTickTimeRef.current = null; expectedTickTimeRef.current = null;
setIsPlaying(false); setIsPlaying(false);
isPlayingRef.current = false; isPlayingRef.current = false;
onPauseRef.current?.(currentIndexRef.current);
}, []); }, []);
const togglePlay = useCallback(() => { const togglePlay = useCallback(() => {
@@ -174,6 +200,7 @@ export function useReaderEngine({
setCurrentIndex(target); setCurrentIndex(target);
currentIndexRef.current = target; currentIndexRef.current = target;
expectedTickTimeRef.current = null; expectedTickTimeRef.current = null;
onJumpRef.current?.(target);
if (isPlayingRef.current) { if (isPlayingRef.current) {
if (timerRef.current) { if (timerRef.current) {
@@ -223,6 +250,7 @@ export function useReaderEngine({
pause, pause,
togglePlay, togglePlay,
jumpTo, jumpTo,
jumpToIndex: jumpTo,
jumpBy, jumpBy,
restart, restart,
totalWords: words.length, totalWords: words.length,