fix(persistence): strengthen state persistence, clean novel intro, and adjust HUD text
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
=== Read The Well of Ascension online free by Brandon Sanderson - Novel80 ===
|
||||
|
||||
PART ONE
|
||||
|
||||
|
||||
|
||||
66
src/App.tsx
66
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<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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user