Preferences in Settings and a home page the user chooses
Build and Push Docker Image / build-and-push (push) Failing after 20s
Build and Push Docker Image / build-and-push (push) Failing after 20s
Settings gets a Preferences section: theme, default Bible, the Bible new reading plans start in, verse layout, commentary, text size, reading width, audio narrator (the last three stay per device; text size and narrator are now remembered). A Home page setting (My Studies, Bible Reader or Reading Plans) decides where the app opens and where every Home button goes; already being there means My Studies. Back arrows are labelled Home. Tests cover every page leading back. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
760c1e568b
commit
8c4f54e033
15 files changed
+401
-23
No files matched your search
+10
-1
@@ -5,7 +5,16 @@ import { bookOptions, CHAPTER_COUNTS } from '../utils/bibleUtils.jsx';
|
||||
|
||||
export function useAudio(readerBookAbbrev, readerChapter, readerTranslation = 'BSB') {
|
||||
const [audioBook, setAudioBook] = useState(bookOptions[0].abbrev);
|
||||
const [audioNarrator, setAudioNarrator] = useState('souer');
|
||||
const [audioNarrator, setAudioNarratorState] = useState(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('audio-narrator');
|
||||
return ['david', 'hays', 'souer'].includes(saved) ? saved : 'souer';
|
||||
} catch { return 'souer'; }
|
||||
});
|
||||
const setAudioNarrator = (id) => {
|
||||
setAudioNarratorState(id);
|
||||
try { localStorage.setItem('audio-narrator', id); } catch { /* storage unavailable */ }
|
||||
};
|
||||
const [audioState, setAudioState] = useState({ status: 'idle', chapter: 0, total: 0 });
|
||||
const [readerAudioState, setReaderAudioState] = useState({ status: 'idle', chapter: 0 });
|
||||
const [audioProgress, setAudioProgress] = useState({ currentTime: 0, duration: 0 });
|
||||
|
||||
@@ -73,7 +73,46 @@ export function useBibleReader(currentPage, translationOverride = null) {
|
||||
const [readerInterlinearLoading, setReaderInterlinearLoading] = useState(false);
|
||||
const [readerSelectedVerse, setReaderSelectedVerse] = useState(null); // verse number with open interlinear panel
|
||||
const _readerInterlinearCacheRef = useRef({});
|
||||
const [readerFontSize, setReaderFontSize] = useState(1); // em multiplier: 0.875 | 1 | 1.125 | 1.25
|
||||
// Text size and reading width are per-device (a phone and a tablet want different ones)
|
||||
const [readerFontSize, setReaderFontSizeState] = useState(() => {
|
||||
try {
|
||||
const n = Number(localStorage.getItem('reader-font-size'));
|
||||
return [0.875, 1, 1.125, 1.25, 1.5].includes(n) ? n : 1;
|
||||
} catch { return 1; }
|
||||
}); // em multiplier: 0.875 | 1 | 1.125 | 1.25 | 1.5
|
||||
const setReaderFontSize = (n) => {
|
||||
setReaderFontSizeState(n);
|
||||
try { localStorage.setItem('reader-font-size', String(n)); } catch {}
|
||||
};
|
||||
const [readerWide, setReaderWideState] = useState(() => {
|
||||
try { return localStorage.getItem('reader-wide') === '1'; } catch { return false; }
|
||||
});
|
||||
const setReaderWide = (on) => {
|
||||
setReaderWideState(!!on);
|
||||
try { localStorage.setItem('reader-wide', on ? '1' : '0'); } catch {}
|
||||
};
|
||||
// The page the app opens to, and where every Home button goes: 'studies' | 'reader' | 'plans'
|
||||
const [startPagePref, setStartPagePrefState] = useState(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('start-page');
|
||||
return ['studies', 'reader', 'plans'].includes(saved) ? saved : 'studies';
|
||||
} catch { return 'studies'; }
|
||||
});
|
||||
const setStartPagePref = (id) => {
|
||||
setStartPagePrefState(id);
|
||||
try { localStorage.setItem('start-page', id); } catch {}
|
||||
};
|
||||
// Which Bible a newly started reading plan begins in: 'same' = whatever the default Bible is
|
||||
const [planTranslationPref, setPlanTranslationPrefState] = useState(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('plan-translation');
|
||||
return saved === 'same' || READER_TRANSLATIONS.some((t) => t.id === saved) ? saved : 'same';
|
||||
} catch { return 'same'; }
|
||||
});
|
||||
const setPlanTranslationPref = (id) => {
|
||||
setPlanTranslationPrefState(id);
|
||||
try { localStorage.setItem('plan-translation', id); } catch {}
|
||||
};
|
||||
const [readerBookmarks, setReaderBookmarks] = useState(() => {
|
||||
try { return JSON.parse(localStorage.getItem('reader-bookmarks') || '{}'); } catch { return {}; }
|
||||
});
|
||||
@@ -397,12 +436,15 @@ export function useBibleReader(currentPage, translationOverride = null) {
|
||||
const prefsSync = usePrefsSync({
|
||||
values: {
|
||||
theme: readerTheme, translation: baseTranslation, layout: readerLayout, commentarySource: readerCommentary,
|
||||
planTranslation: planTranslationPref, startPage: startPagePref,
|
||||
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 === 'startPage') { setStartPagePrefState(v); try { localStorage.setItem('start-page', v); } catch {} }
|
||||
else if (key === 'planTranslation') { setPlanTranslationPrefState(v); try { localStorage.setItem('plan-translation', 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); }
|
||||
},
|
||||
@@ -424,6 +466,9 @@ export function useBibleReader(currentPage, translationOverride = null) {
|
||||
readerInterlinear,
|
||||
readerSelectedVerse, setReaderSelectedVerse,
|
||||
readerFontSize, setReaderFontSize,
|
||||
readerWide, setReaderWide,
|
||||
planTranslationPref, setPlanTranslationPref,
|
||||
startPagePref, setStartPagePref,
|
||||
readerBookmarks, setReaderBookmarks,
|
||||
readerBookmarksPanelOpen, setReaderBookmarksPanelOpen,
|
||||
readerCrossRefs, setReaderCrossRefs,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { getRemotePrefs, saveRemotePrefs } from '../syncService.js';
|
||||
|
||||
export const PREF_KEYS = ['theme', 'translation', 'layout', 'commentarySource', 'position'];
|
||||
export const PREF_KEYS = ['theme', 'translation', 'layout', 'commentarySource', 'planTranslation', 'startPage', 'position'];
|
||||
const META_KEY = 'prefs-meta'; // { key: timestamp of the last change made on this device }
|
||||
const PUSH_DELAY_MS = 1000;
|
||||
const FOCUS_PULL_MIN_MS = 30_000;
|
||||
@@ -16,7 +16,7 @@ const lsDel = (k) => { try { localStorage.removeItem(k); } catch { /* storage un
|
||||
const readMeta = () => { try { return JSON.parse(lsGet(META_KEY) || '{}'); } catch { return {}; } };
|
||||
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
||||
|
||||
// values: { theme, translation, layout, commentarySource, position: { book, chapter } }
|
||||
// values: { theme, translation, layout, commentarySource, planTranslation, startPage, position: { book, chapter } }
|
||||
// apply(key, value): puts a server value on screen and in localStorage
|
||||
// canApplyPosition(): false while the Reader is open, so a background sync never moves the page under you
|
||||
export function usePrefsSync({ values, apply, canApplyPosition = () => true }) {
|
||||
@@ -58,7 +58,7 @@ export function usePrefsSync({ values, apply, canApplyPosition = () => true }) {
|
||||
window.clearTimeout(timerRef.current);
|
||||
timerRef.current = window.setTimeout(push, PUSH_DELAY_MS);
|
||||
}
|
||||
}, [values.theme, values.translation, values.layout, values.commentarySource, values.position.book, values.position.chapter, push]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [values.theme, values.translation, values.layout, values.commentarySource, values.planTranslation, values.startPage, values.position.book, values.position.chapter, push]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const pullServerPrefs = useCallback(async () => {
|
||||
enabledRef.current = true;
|
||||
|
||||
@@ -19,7 +19,7 @@ function setup({ canApplyPosition = () => true, initial = {} } = {}) {
|
||||
const [layout, setLayout] = useState(initial.layout ?? 'flow');
|
||||
const [position, setPosition] = useState(initial.position ?? { book: 'GEN', chapter: 1 });
|
||||
const sync = usePrefsSync({
|
||||
values: { theme, translation: 'BSB', layout, commentarySource: 'matthew-henry', position },
|
||||
values: { theme, translation: 'BSB', layout, commentarySource: 'matthew-henry', planTranslation: 'same', startPage: 'studies', position },
|
||||
apply: (key, v) => { if (key === 'theme') setTheme(v); else if (key === 'layout') setLayout(v); else if (key === 'position') setPosition(v); },
|
||||
canApplyPosition,
|
||||
});
|
||||
@@ -45,7 +45,7 @@ test('a device with no history takes the account\'s settings; a first device upl
|
||||
await act(async () => { await h.result.current.pullServerPrefs(); });
|
||||
expect(h.result.current.theme).toBe('sepia');
|
||||
// keys the account doesn't have yet are filled in from this device
|
||||
expect(Object.keys(remote.puts[0]).sort()).toEqual(['commentarySource', 'layout', 'position', 'translation']);
|
||||
expect(Object.keys(remote.puts[0]).sort()).toEqual(['commentarySource', 'layout', 'planTranslation', 'position', 'startPage', 'translation']);
|
||||
});
|
||||
|
||||
test('reading position is not moved under you while the Reader is open', async () => {
|
||||
|
||||
Reference in new issue
Block a user