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

View File

@@ -62,35 +62,54 @@ export default function App() {
saveState({ fontSize: newSize });
};
// Throttled persistence of wordIndex to localStorage
// Robust state persistence
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) => {
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() {
<span className="text-zinc-400 font-semibold">
{wpm} WPM{' '}
<span className="font-normal text-zinc-500 text-[10px]">
({flashesPerMinute} FPM × {wordsPerChunk})
({flashesPerMinute} FPM × {wordsPerChunk} 词)
</span>
</span>
</div>

View File

@@ -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<number>(() =>
Math.max(0, initialWordIndex || 0)
@@ -51,6 +55,23 @@ export function useReaderEngine({
const expectedTickTimeRef = useRef<number | 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
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,