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
|
PART ONE
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
66
src/App.tsx
66
src/App.tsx
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user