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
@@ -39,6 +39,7 @@ export default function ReaderPage() {
|
||||
readerInterlinear,
|
||||
readerSelectedVerse, setReaderSelectedVerse,
|
||||
readerFontSize, setReaderFontSize,
|
||||
readerWide: readerWideLayout, setReaderWide: setReaderWideLayout,
|
||||
readerBookmarks,
|
||||
readerCrossRefs, setReaderCrossRefs,
|
||||
readerCrossRefsLoading,
|
||||
@@ -84,12 +85,10 @@ export default function ReaderPage() {
|
||||
const [sheet, setSheet] = useState(null); // null | 'nav' | 'settings' | 'search' | 'bookmarks' | 'audio'
|
||||
const [chromeVisible, setChromeVisible] = useState(true);
|
||||
const [readerDrawMode, setReaderDrawMode] = useState(false);
|
||||
const [readerWideLayout, setReaderWideLayout] = useState(() => localStorage.getItem('reader-wide') === '1');
|
||||
const [selectionPicker, setSelectionPicker] = useState(null);
|
||||
const swipeTouchRef = useRef(null);
|
||||
const readerPageInkStrokes = readerInkByPage?.[`${readerBookAbbrev}_${readerChapter}`] ?? NO_STROKES;
|
||||
|
||||
useEffect(() => { localStorage.setItem('reader-wide', readerWideLayout ? '1' : '0'); }, [readerWideLayout]);
|
||||
|
||||
const chapterKey = `${readerBookAbbrev}.${readerChapter}`;
|
||||
const label = (ref) => formatDayReadings([ref]);
|
||||
|
||||
@@ -12,7 +12,7 @@ const ctx = {
|
||||
readerVerses: [{ number: 1, text: 'Now there was a man of the Pharisees.' }, { number: 2, text: 'This man came to Jesus by night.' }],
|
||||
readerTotalChapters: 21, readerLoading: false, readerError: '',
|
||||
readerInterlinear: null, readerSelectedVerse: null, setReaderSelectedVerse: vi.fn(),
|
||||
readerFontSize: 1, setReaderFontSize: noop, readerBookmarks: {},
|
||||
readerFontSize: 1, setReaderFontSize: noop, readerWide: false, setReaderWide: noop, readerBookmarks: {},
|
||||
readerCrossRefs: null, setReaderCrossRefs: noop, readerCrossRefsLoading: false,
|
||||
readerShowCrossRefs: false, setReaderShowCrossRefs: noop,
|
||||
readerSearch: '', setReaderSearch: noop, setReaderSearchActive: noop,
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function ReadingPlansPage() {
|
||||
addCustomPlan, removeCustomPlan, updateCustomPlan,
|
||||
openPlanReading, studyReadingPlanDay, studyBuildBusy,
|
||||
readingPlanSyncStatus, readingPlanSyncError, retryReadingPlanSync,
|
||||
readerTheme, setReaderTheme, readerTranslation,
|
||||
readerTheme, setReaderTheme, readerTranslation, planTranslationPref,
|
||||
} = useApp();
|
||||
|
||||
const [detail, setDetail] = useState(null); // { planId, progressId? }
|
||||
@@ -62,7 +62,7 @@ export default function ReadingPlansPage() {
|
||||
const cycleTheme = () => setReaderTheme(THEME_ORDER[(THEME_ORDER.indexOf(readerTheme) + 1) % THEME_ORDER.length]);
|
||||
|
||||
const actions = {
|
||||
defaultTranslation: readerTranslation,
|
||||
defaultTranslation: planTranslationPref && planTranslationPref !== 'same' ? planTranslationPref : readerTranslation,
|
||||
setTranslation: setPlanTranslation,
|
||||
toggleDay,
|
||||
recalibrate: recalibratePace,
|
||||
@@ -103,7 +103,7 @@ export default function ReadingPlansPage() {
|
||||
<div className="min-h-screen bg-[var(--r-bg)] pb-16 text-[var(--r-text)]" style={themeVars(readerTheme)}>
|
||||
<div className="sticky top-0 z-30 border-b border-[var(--r-line)] bg-[var(--r-bar)] font-sans">
|
||||
<div className="mx-auto flex h-12 max-w-3xl items-center gap-1 px-2">
|
||||
<button type="button" onClick={goHome} aria-label="Back to studies" className="flex h-9 w-9 items-center justify-center rounded-lg text-[var(--r-sub)] hover:bg-black/5"><ChevronLeft /></button>
|
||||
<button type="button" onClick={goHome} aria-label="Back to home" className="flex h-9 items-center gap-0.5 rounded-lg pl-1 pr-2 text-[var(--r-sub)] hover:bg-black/5"><ChevronLeft /><span className="hidden text-xs sm:inline">Home</span></button>
|
||||
<h1 className="mx-auto text-sm font-medium">Reading plans</h1>
|
||||
{streak.current > 0 && <span className="text-xs text-[var(--r-accent)]">{streak.current} day streak</span>}
|
||||
<button type="button" onClick={cycleTheme} className="ml-2 rounded-lg px-2 py-1.5 text-xs text-[var(--r-sub)] hover:bg-black/5" title="Change theme">{READER_THEMES[readerTheme]?.label}</button>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
import { useApp } from '../context/AppContext.js';
|
||||
import PageShell from '../components/study/PageShell.jsx';
|
||||
import { READER_TRANSLATIONS } from '../hooks/useBibleReader.js';
|
||||
import { COMMENTARIES } from '../hooks/useVerseStudy.js';
|
||||
import { READER_THEMES } from '../components/reader/readerThemes.js';
|
||||
import {
|
||||
startMfaSetup,
|
||||
confirmMfaSetup,
|
||||
@@ -12,10 +15,46 @@ import {
|
||||
} from '../syncService.js';
|
||||
import { exportJournalDocx, printJournalPdf } from '../utils/journalExport.js';
|
||||
|
||||
const FONT_SIZES = [['S', 0.875], ['M', 1], ['L', 1.125], ['XL', 1.25], ['XXL', 1.5]];
|
||||
const selectCls = 'rounded-xl border border-[var(--r-line)] bg-[var(--r-bg)] px-3 py-2 text-sm text-[var(--r-text)] focus:outline-none focus:ring-2 focus:ring-[var(--r-line)]';
|
||||
|
||||
// One labelled preference: what it is, whether it follows you between devices, and the control.
|
||||
function PrefRow({ label, hint, synced, children }) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-between gap-x-6 gap-y-2 border-t border-[var(--r-line)] py-3 first:border-t-0 first:pt-0">
|
||||
<div className="min-w-0 flex-1 basis-56">
|
||||
<p className="text-sm font-medium">{label}</p>
|
||||
<p className="text-xs text-[var(--r-sub)]">{hint} <span className="whitespace-nowrap">· {synced ? 'Synced to your account' : 'This device only'}</span></p>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Segmented({ label, options, value, onChange }) {
|
||||
return (
|
||||
<div role="group" aria-label={label} className="flex overflow-hidden rounded-xl border border-[var(--r-line)]">
|
||||
{options.map(([id, text]) => (
|
||||
<button key={String(id)} type="button" aria-pressed={value === id} onClick={() => onChange(id)}
|
||||
className={`px-3 py-1.5 text-sm ${value === id ? 'bg-[var(--r-text)] text-[var(--r-bg)]' : 'text-[var(--r-sub)] hover:bg-black/5'}`}>{text}</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
const {
|
||||
authUser, setAuthUser,
|
||||
goHome,
|
||||
readerTheme, setReaderTheme,
|
||||
readerTranslation, setReaderTranslation,
|
||||
planTranslationPref, setPlanTranslationPref,
|
||||
startPagePref, setStartPagePref,
|
||||
readerLayout, setReaderLayout,
|
||||
readerFontSize, setReaderFontSize,
|
||||
readerWide, setReaderWide,
|
||||
readerCommentary, setReaderCommentary,
|
||||
audioNarrator, setAudioNarrator,
|
||||
} = useApp();
|
||||
const [mfaSetup, setMfaSetup] = useState(null); // { secret, qrCodeDataUrl } while setup is in progress
|
||||
const [mfaSetupCode, setMfaSetupCode] = useState('');
|
||||
@@ -179,6 +218,49 @@ export default function SettingsPage() {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section aria-label="Preferences" className="rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-6">
|
||||
<h2 className="text-base font-medium">Preferences</h2>
|
||||
<p className="mb-4 mt-1 text-sm text-[var(--r-sub)]">How the app looks and reads. Study projects always use the Berean Standard Bible.</p>
|
||||
<PrefRow label="Home page" hint="Where the app opens, and where the Home buttons take you." synced>
|
||||
<Segmented label="Home page" options={[['studies', 'My Studies'], ['reader', 'Bible Reader'], ['plans', 'Reading Plans']]} value={startPagePref} onChange={setStartPagePref} />
|
||||
</PrefRow>
|
||||
<PrefRow label="Theme" hint="Paper, sepia or dark." synced>
|
||||
<Segmented label="Theme" options={Object.entries(READER_THEMES).map(([id, t]) => [id, t.label])} value={readerTheme} onChange={setReaderTheme} />
|
||||
</PrefRow>
|
||||
<PrefRow label="Default Bible" hint="What the Reader opens in." synced>
|
||||
<select aria-label="Default Bible" value={readerTranslation} onChange={(e) => setReaderTranslation(e.target.value)} className={selectCls}>
|
||||
{READER_TRANSLATIONS.map((t) => <option key={t.id} value={t.id}>{t.id} · {t.name}</option>)}
|
||||
</select>
|
||||
</PrefRow>
|
||||
<PrefRow label="New reading plans" hint="The Bible a plan starts in. You can change it per plan." synced>
|
||||
<select aria-label="New reading plans" value={planTranslationPref} onChange={(e) => setPlanTranslationPref(e.target.value)} className={selectCls}>
|
||||
<option value="same">Same as my default Bible</option>
|
||||
{READER_TRANSLATIONS.map((t) => <option key={t.id} value={t.id}>{t.id} · {t.name}</option>)}
|
||||
</select>
|
||||
</PrefRow>
|
||||
<PrefRow label="Verse layout" hint="Verses run together like a page, or one to a line." synced>
|
||||
<Segmented label="Verse layout" options={[['flow', 'Flowing'], ['verses', 'One per line']]} value={readerLayout} onChange={setReaderLayout} />
|
||||
</PrefRow>
|
||||
<PrefRow label="Commentary" hint="The one shown first in the Reader and the Study stage." synced>
|
||||
<select aria-label="Commentary" value={readerCommentary} onChange={(e) => setReaderCommentary(e.target.value)} className={selectCls}>
|
||||
{COMMENTARIES.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||
</select>
|
||||
</PrefRow>
|
||||
<PrefRow label="Text size" hint="Reader text.">
|
||||
<Segmented label="Text size" options={FONT_SIZES.map(([l, n]) => [n, l])} value={readerFontSize} onChange={setReaderFontSize} />
|
||||
</PrefRow>
|
||||
<PrefRow label="Reading width" hint="One column, or two on a wide screen.">
|
||||
<Segmented label="Reading width" options={[[false, 'Column'], [true, 'Two columns']]} value={readerWide} onChange={setReaderWide} />
|
||||
</PrefRow>
|
||||
<PrefRow label="Audio narrator" hint="For the Berean Standard Bible audio.">
|
||||
<select aria-label="Audio narrator" value={audioNarrator} onChange={(e) => setAudioNarrator(e.target.value)} className={selectCls}>
|
||||
<option value="david">David</option>
|
||||
<option value="hays">Hays</option>
|
||||
<option value="souer">Souer</option>
|
||||
</select>
|
||||
</PrefRow>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-6 space-y-4">
|
||||
<div>
|
||||
<h2 className="text-base font-medium text-[var(--r-text)]">Change password</h2>
|
||||
|
||||
Reference in new issue
Block a user