Sync bookmarks, highlights and display preferences to the account
Build and Push Docker Image / build-and-push (push) Failing after 19s

Bookmarks and text highlights are saved per user and merged across devices
(union on first contact, newer server copy when this device is clean). Theme,
default translation, verse layout, commentary source and reading position sync
too, each with its own timestamp so the newest change to each setting wins.
The Reader never moves under you while it is open. Signing out clears this
device's marks and forgets sync timestamps.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-09-29 10:45:23 -04:00
1 parent 297f2c7e84
commit 760c1e568b
20 files changed
+976 -18

No files matched your search

+53 -2
View File
@@ -3,6 +3,9 @@
// the same <audio> element.
import { useCallback, useEffect, useRef, useState } from 'react';
import { bookOptions, CHAPTER_COUNTS, parseBibleChapter } from '../utils/bibleUtils.jsx';
import { useReaderMarksSync } from './useReaderMarksSync.js';
import { usePrefsSync } from './usePrefsSync.js';
import { COMMENTARIES } from './useVerseStudy.js';
import { BIBLE_IDB_TTL, getBibleIndexFromIDB, saveBibleIndexToIDB } from '../utils/bibleIndexCache.js';
export const READER_TRANSLATIONS = [
@@ -24,8 +27,26 @@ const BOOKMARK_COLORS = ['#fef08a', '#bbf7d0', '#bfdbfe', '#fecaca', '#e9d5ff'];
// `translationOverride` lets a reading plan pin the Bible it is read in while it is
// open; otherwise the reader's own (remembered) translation is used.
export function useBibleReader(currentPage, translationOverride = null) {
const [readerBookAbbrev, setReaderBookAbbrev] = useState(bookOptions[0].abbrev);
const [readerChapter, setReaderChapter] = useState(1);
// Where you left off (saved on this device, and synced to the account)
const [savedPosition] = useState(() => {
try {
const p = JSON.parse(localStorage.getItem('reader-position') || 'null');
return p && CHAPTER_COUNTS[p.book] && Number.isInteger(p.chapter) && p.chapter >= 1 && p.chapter <= CHAPTER_COUNTS[p.book] ? p : null;
} catch { return null; }
});
const [readerBookAbbrev, setReaderBookAbbrev] = useState(savedPosition?.book ?? bookOptions[0].abbrev);
const [readerChapter, setReaderChapter] = useState(savedPosition?.chapter ?? 1);
const [readerCommentary, setReaderCommentaryState] = useState(() => {
try {
const saved = localStorage.getItem('reader-commentary');
return COMMENTARIES.some((c) => c.id === saved) ? saved : COMMENTARIES[0].id;
} catch { return COMMENTARIES[0].id; }
});
useEffect(() => {
try { localStorage.setItem('reader-position', JSON.stringify({ book: readerBookAbbrev, chapter: readerChapter })); } catch {}
}, [readerBookAbbrev, readerChapter]);
const pageRef = useRef(currentPage);
pageRef.current = currentPage;
const [baseTranslation, setReaderTranslationState] = useState(() => {
try {
const saved = localStorage.getItem('reader-translation');
@@ -138,6 +159,11 @@ export function useBibleReader(currentPage, translationOverride = null) {
try { localStorage.setItem('reader-layout', id); } catch {}
};
const setReaderCommentary = (id) => {
setReaderCommentaryState(id);
try { localStorage.setItem('reader-commentary', id); } catch {}
};
const setReaderTranslation = (id) => {
setReaderTranslationState(id);
try { localStorage.setItem('reader-translation', id); } catch {}
@@ -361,8 +387,31 @@ export function useBibleReader(currentPage, translationOverride = null) {
});
}, []);
// Bookmarks and highlights follow the signed-in user across devices.
const marksSync = useReaderMarksSync({
bookmarks: readerBookmarks, highlights: readerTextHighlights,
setBookmarks: setReaderBookmarks, setHighlights: setReaderTextHighlights,
});
// Theme, default translation, layout, commentary and reading position follow the account too.
const prefsSync = usePrefsSync({
values: {
theme: readerTheme, translation: baseTranslation, layout: readerLayout, commentarySource: readerCommentary,
position: { book: readerBookAbbrev, chapter: readerChapter },
},
apply: (key, v) => {
if (key === 'theme') { setReaderThemeState(v); try { localStorage.setItem('reader-theme', v); } catch {} }
else if (key === 'translation') { setReaderTranslationState(v); try { localStorage.setItem('reader-translation', v); } catch {} }
else if (key === 'layout') { setReaderLayoutState(v); try { localStorage.setItem('reader-layout', v); } catch {} }
else if (key === 'commentarySource') { setReaderCommentaryState(v); try { localStorage.setItem('reader-commentary', v); } catch {} }
else if (key === 'position') { setReaderBookAbbrev(v.book); setReaderChapter(v.chapter); setReaderSelectedVerse(null); }
},
canApplyPosition: () => pageRef.current !== 'reader',
});
return {
readerBookAbbrev, setReaderBookAbbrev,
readerCommentary, setReaderCommentary,
readerChapter, setReaderChapter,
readerTranslation, setReaderTranslation,
readerCopyright,
@@ -387,6 +436,8 @@ export function useBibleReader(currentPage, translationOverride = null) {
updateReaderPageInk,
mergeServerInk,
readerTextHighlights,
...marksSync,
...prefsSync,
addTextHighlight,
removeTextHighlight,
bibleIndexStatusFor,