Skip to content

Commit 69e4fde

Browse files
committed
Fix book text persistence on iOS by using IndexedDB storage
1 parent 2ee6089 commit 69e4fde

1 file changed

Lines changed: 32 additions & 32 deletions

File tree

src/App.jsx

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,11 @@ function loadSettings() {
118118

119119
function saveSettings(settings) {
120120
try {
121-
// Store text in IndexedDB, not localStorage
122-
const { text, ...rest } = settings;
121+
// Store text and position in IndexedDB, small settings in localStorage
122+
const { text, currentIndex, ...rest } = settings;
123123
localStorage.setItem(STORAGE_KEY, JSON.stringify(rest));
124124
idbSet("text", text).catch(() => {});
125+
idbSet("position", currentIndex).catch(() => {});
125126
} catch (e) {
126127
console.error("Failed to save settings:", e);
127128
}
@@ -493,6 +494,9 @@ const BookView = memo(function BookView({ words, currentIndex, sideOpacity, setC
493494
);
494495
});
495496

497+
// Capture URL hash before any effects can modify it
498+
const initialUrlHash = window.location.hash;
499+
496500
function App() {
497501
// Load settings only once on mount
498502
const [savedSettings] = useState(() => loadSettings());
@@ -534,31 +538,26 @@ function App() {
534538
return clamp(getPositionForText(savedSettings?.text || DEFAULT_TEXT, savedSettings?.positions || {}));
535539
});
536540

537-
// Load text from IndexedDB on mount (async, overrides default text)
541+
// Load text and position from IndexedDB on mount
538542
const [idbLoaded, setIdbLoaded] = useState(false);
539543
useEffect(() => {
540-
idbGet("text").then((savedText) => {
544+
Promise.all([idbGet("text"), idbGet("position")]).then(([savedText, savedPos]) => {
541545
if (savedText && savedText !== DEFAULT_TEXT) {
542546
setText(savedText);
543547
const parsed = parseText(savedText);
544548
setWords(parsed.words);
545549
setParagraphBreaks(parsed.breaks);
546-
// Restore position for this text
547-
const hash = window.location.hash;
550+
// Restore position: URL hash (captured before effects) > IndexedDB > 0
548551
let pos = 0;
549-
if (hash) {
550-
const params = new URLSearchParams(hash.slice(1));
552+
if (initialUrlHash) {
553+
const params = new URLSearchParams(initialUrlHash.slice(1));
551554
const urlPos = parseInt(params.get("pos"), 10);
552555
if (!isNaN(urlPos) && urlPos >= 0) pos = urlPos;
553-
} else {
554-
try {
555-
const stored = localStorage.getItem("rsvp-current-index");
556-
if (stored != null) pos = parseInt(stored, 10) || 0;
557-
} catch {}
556+
} else if (typeof savedPos === "number") {
557+
pos = savedPos;
558558
}
559559
const clamped = Math.min(Math.max(0, pos), Math.max(0, parsed.words.length - 1));
560560
_setCurrentIndex(clamped);
561-
try { localStorage.setItem("rsvp-current-index", String(clamped)); } catch {}
562561
}
563562
setIdbLoaded(true);
564563
}).catch(() => setIdbLoaded(true));
@@ -669,35 +668,36 @@ function App() {
669668

670669

671670

672-
// Save settings including position for current text
673671
// Save position for current text (lightweight, every word change)
674672
useEffect(() => {
673+
if (!idbLoaded) return;
675674
positionsRef.current = savePositionForText(
676675
text,
677676
currentIndex,
678677
positionsRef.current,
679678
);
680679
try { localStorage.setItem("rsvp-current-index", String(currentIndex)); } catch {}
681-
}, [text, currentIndex]);
680+
}, [text, currentIndex, idbLoaded]);
682681

683682
// Save full settings when settings/text change or playback stops
684683
useEffect(() => {
685-
if (!isPlaying) {
686-
saveSettings({
687-
wpm,
688-
text,
689-
currentIndex,
690-
positions: positionsRef.current,
691-
sideOpacity,
692-
bookView,
693-
bookMetadata,
694-
fetchMetadataOnline,
695-
});
696-
}
697-
}, [wpm, text, isPlaying, sideOpacity, bookView, bookMetadata, fetchMetadataOnline]);
684+
if (!idbLoaded || isPlaying) return;
685+
saveSettings({
686+
wpm,
687+
text,
688+
currentIndex,
689+
positions: positionsRef.current,
690+
sideOpacity,
691+
bookView,
692+
bookMetadata,
693+
fetchMetadataOnline,
694+
});
695+
}, [wpm, text, isPlaying, sideOpacity, bookView, bookMetadata, fetchMetadataOnline, idbLoaded]);
698696

697+
// Save full settings when page unloads or goes to background (iOS)
699698
// Save full settings when page unloads or goes to background (iOS)
700699
useEffect(() => {
700+
if (!idbLoaded) return;
701701
const save = () => {
702702
saveSettings({
703703
wpm,
@@ -719,14 +719,14 @@ function App() {
719719
window.removeEventListener("beforeunload", save);
720720
document.removeEventListener("visibilitychange", handleVisibility);
721721
};
722-
}, [wpm, text, currentIndex, sideOpacity, bookView, bookMetadata, fetchMetadataOnline]);
722+
}, [wpm, text, currentIndex, sideOpacity, bookView, bookMetadata, fetchMetadataOnline, idbLoaded]);
723723

724724
// Update URL hash with current position in real time
725725
useEffect(() => {
726-
if (words.length > 0) {
726+
if (idbLoaded && words.length > 0) {
727727
window.history.replaceState(null, "", `${window.location.pathname}#pos=${currentIndex}`);
728728
}
729-
}, [currentIndex, words.length]);
729+
}, [currentIndex, words.length, idbLoaded]);
730730

731731
const getBaseDelay = useCallback(() => {
732732
return (60 / wpm) * 1000;

0 commit comments

Comments
 (0)