diff --git a/src/pages/ReaderPage.jsx b/src/pages/ReaderPage.jsx
index 0ea7b8c..4e20343 100644
--- a/src/pages/ReaderPage.jsx
+++ b/src/pages/ReaderPage.jsx
@@ -58,6 +58,12 @@ export default function ReaderPage() {
const swipeTouchRef = useRef(null);
const [toolbarVisible, setToolbarVisible] = useState(true);
+ const [readerPageMode, setReaderPageMode] = useState(() => localStorage.getItem('reader-page-mode') === '1');
+ const [flipPage, setFlipPage] = useState(0);
+ const [flipTotalPages, setFlipTotalPages] = useState(1);
+ const [flipPageWidth, setFlipPageWidth] = useState(0);
+ const flipContainerRef = useRef(null);
+ const flipContentRef = useRef(null);
useEffect(() => {
localStorage.setItem('reader-wide', readerWideLayout ? '1' : '0');
@@ -65,6 +71,36 @@ export default function ReaderPage() {
setToolbarVisible(!readerWideLayout);
}, [readerWideLayout]);
+ useEffect(() => {
+ localStorage.setItem('reader-page-mode', readerPageMode ? '1' : '0');
+ }, [readerPageMode]);
+
+ // Reset to page 0 when navigating chapters
+ useEffect(() => {
+ setFlipPage(0);
+ }, [readerChapter, readerBookAbbrev]);
+
+ // Track container width for CSS column sizing
+ useEffect(() => {
+ if (!readerPageMode || !flipContainerRef.current) return;
+ const el = flipContainerRef.current;
+ const update = () => setFlipPageWidth(el.clientWidth);
+ update();
+ const ro = new ResizeObserver(update);
+ ro.observe(el);
+ return () => ro.disconnect();
+ }, [readerPageMode, readerVerses]);
+
+ // Count pages after content renders
+ useEffect(() => {
+ if (!readerPageMode || !flipContentRef.current || !flipPageWidth) return;
+ const t = setTimeout(() => {
+ const sw = flipContentRef.current?.scrollWidth ?? 0;
+ setFlipTotalPages(Math.max(1, Math.round(sw / flipPageWidth)));
+ }, 120);
+ return () => clearTimeout(t);
+ }, [readerPageMode, readerVerses, readerFontSize, flipPageWidth]);
+
// Swipe left/right to navigate chapters
const handleTouchStart = (e) => {
if (readerDrawMode) return;
@@ -78,20 +114,30 @@ export default function ReaderPage() {
const dy = t.clientY - swipeTouchRef.current.y;
swipeTouchRef.current = null;
if (Math.abs(dx) < 60 || Math.abs(dy) > Math.abs(dx) * 0.8) return;
- if (dx < 0) readerGoToNextChapter();
- else readerGoToPreviousChapter();
+ if (dx < 0) {
+ if (readerPageMode && flipPage < flipTotalPages - 1) setFlipPage((p) => p + 1);
+ else readerGoToNextChapter();
+ } else {
+ if (readerPageMode && flipPage > 0) setFlipPage((p) => p - 1);
+ else readerGoToPreviousChapter();
+ }
};
// Arrow keys for desktop
useEffect(() => {
const handleKey = (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
- if (e.key === 'ArrowLeft') readerGoToPreviousChapter();
- else if (e.key === 'ArrowRight') readerGoToNextChapter();
+ if (e.key === 'ArrowLeft') {
+ if (readerPageMode && flipPage > 0) setFlipPage((p) => p - 1);
+ else readerGoToPreviousChapter();
+ } else if (e.key === 'ArrowRight') {
+ if (readerPageMode && flipPage < flipTotalPages - 1) setFlipPage((p) => p + 1);
+ else readerGoToNextChapter();
+ }
};
window.addEventListener('keydown', handleKey);
return () => window.removeEventListener('keydown', handleKey);
- }, [readerGoToPreviousChapter, readerGoToNextChapter]);
+ }, [readerGoToPreviousChapter, readerGoToNextChapter, readerPageMode, flipPage, flipTotalPages]);
// Dismiss the highlight picker when tapping outside it
useEffect(() => {
@@ -360,6 +406,15 @@ export default function ReaderPage() {
✕ Hide
)}
+ {!readerDrawMode && (
+
+ )}
{/* end collapsible toolbar */}
@@ -445,128 +500,283 @@ export default function ReaderPage() {
- {/* Verses — hidden when draw mode is active */}
-
-
- {readerBook?.name} {readerChapter} (BSB)
-
- {readerInterlinear && (
-
Click a verse number to see original words & pronunciation · bookmark icon to save · copy icon to copy
- )}
- {!readerInterlinear && (
-
Hover a verse for actions
- )}
- {readerLoading &&
Loading…
}
- {readerError &&
{readerError}
}
- {!readerLoading && !readerError && (() => {
- const searchLower = readerSearchScope === 'chapter' ? readerSearch.trim().toLowerCase() : '';
- const filtered = searchLower
- ? readerVerses.filter((v) => v.text.toLowerCase().includes(searchLower))
- : readerVerses;
- if (searchLower && filtered.length === 0) {
- return
No verses match "{readerSearch}".
;
- }
- return (
-
- {filtered.map((verse) => {
- const chapterInterlinear = readerInterlinear?.[String(readerChapter)];
- const verseWords = chapterInterlinear?.[String(verse.number)];
- const isOpen = readerSelectedVerse === verse.number;
- const verseKey = `${readerBookAbbrev}-${readerChapter}-${verse.number}`;
- const bmColor = readerBookmarks[verseKey];
- const crossRefs = readerCrossRefs?.[verse.number];
+ {/* Verses — normal scroll mode */}
+ {!readerDrawMode && !readerPageMode && (
+
+
+ {readerBook?.name} {readerChapter} (BSB)
+
+ {readerInterlinear && (
+
Click a verse number to see original words & pronunciation · bookmark icon to save · copy icon to copy
+ )}
+ {!readerInterlinear && (
+
Hover a verse for actions
+ )}
+ {readerLoading &&
Loading…
}
+ {readerError &&
{readerError}
}
+ {!readerLoading && !readerError && (() => {
+ const searchLower = readerSearchScope === 'chapter' ? readerSearch.trim().toLowerCase() : '';
+ const filtered = searchLower
+ ? readerVerses.filter((v) => v.text.toLowerCase().includes(searchLower))
+ : readerVerses;
+ if (searchLower && filtered.length === 0) {
+ return
No verses match "{readerSearch}".
;
+ }
+ return (
+
+ {filtered.map((verse) => {
+ const chapterInterlinear = readerInterlinear?.[String(readerChapter)];
+ const verseWords = chapterInterlinear?.[String(verse.number)];
+ const isOpen = readerSelectedVerse === verse.number;
+ const verseKey = `${readerBookAbbrev}-${readerChapter}-${verse.number}`;
+ const bmColor = readerBookmarks[verseKey];
+ const crossRefs = readerCrossRefs?.[verse.number];
- return (
-
-
-
-
{renderVerseText(verse.number, verse.text, searchLower)}
-
+ return (
+
+
- {bmColor && (
+
{renderVerseText(verse.number, verse.text, searchLower)}
+
- )}
-
-
-
-
- {readerShowCrossRefs && crossRefs && (
-
- {crossRefs.slice(0, 8).map((ref, i) => {
- const label = formatCrossRef(ref);
- return (
-
- {label}
-
- );
- })}
+ {bmColor && (
+
+ )}
+
+
- )}
- {isOpen && verseWords && (
-
-
- {verseWords.map((w, i) => {
- const isHebrew = w.s?.startsWith('H');
- const canSpeak = isHebrew || w.s?.startsWith('G');
+ {readerShowCrossRefs && crossRefs && (
+
+ {crossRefs.slice(0, 8).map((ref, i) => {
+ const label = formatCrossRef(ref);
return (
-
-
- {w.o}
-
-
{w.t}
-
{w.g}
- {w.p &&
{w.p}
}
- {w.s &&
{w.s}
}
- {canSpeak && (
-
- )}
-
+
+ {label}
+
);
})}
+ )}
+
+ {isOpen && verseWords && (
+
+
+ {verseWords.map((w, i) => {
+ const isHebrew = w.s?.startsWith('H');
+ const canSpeak = isHebrew || w.s?.startsWith('G');
+ return (
+
+
+ {w.o}
+
+
{w.t}
+
{w.g}
+ {w.p &&
{w.p}
}
+ {w.s &&
{w.s}
}
+ {canSpeak && (
+
+ )}
+
+ );
+ })}
+
+
+ )}
+
+ );
+ })}
+
+ );
+ })()}
+
+ )}
+
+ {/* Verses — page-flip mode */}
+ {!readerDrawMode && readerPageMode && (
+
+ {readerLoading &&
Loading…
}
+ {readerError &&
{readerError}
}
+ {!readerLoading && !readerError && (() => {
+ const searchLower = readerSearchScope === 'chapter' ? readerSearch.trim().toLowerCase() : '';
+ const filtered = searchLower
+ ? readerVerses.filter((v) => v.text.toLowerCase().includes(searchLower))
+ : readerVerses;
+ if (searchLower && filtered.length === 0) {
+ return
No verses match "{readerSearch}".
;
+ }
+ return (
+
+
+
+ {readerBook?.name} {readerChapter} (BSB)
+
+ {readerInterlinear
+ ?
Click a verse number to see original words & pronunciation
+ :
Hover a verse for actions
+ }
+
+ {filtered.map((verse) => {
+ const chapterInterlinear = readerInterlinear?.[String(readerChapter)];
+ const verseWords = chapterInterlinear?.[String(verse.number)];
+ const isOpen = readerSelectedVerse === verse.number;
+ const verseKey = `${readerBookAbbrev}-${readerChapter}-${verse.number}`;
+ const bmColor = readerBookmarks[verseKey];
+ const crossRefs = readerCrossRefs?.[verse.number];
+
+ return (
+
+
+
+
{renderVerseText(verse.number, verse.text, searchLower)}
+
+
+ {bmColor && (
+
+ )}
+
+
- )}
-
- );
- })}
-
- );
- })()}
-
+
+ {readerShowCrossRefs && crossRefs && (
+
+ {crossRefs.slice(0, 8).map((ref, i) => {
+ const label = formatCrossRef(ref);
+ return (
+
+ {label}
+
+ );
+ })}
+
+ )}
+
+ {isOpen && verseWords && (
+
+
+ {verseWords.map((w, i) => {
+ const isHebrew = w.s?.startsWith('H');
+ const canSpeak = isHebrew || w.s?.startsWith('G');
+ return (
+
+
+ {w.o}
+
+
{w.t}
+
{w.g}
+ {w.p &&
{w.p}
}
+ {w.s &&
{w.s}
}
+ {canSpeak && (
+
+ )}
+
+ );
+ })}
+
+
+ )}
+
+ );
+ })}
+
+ );
+ })()}
+ {/* Page navigation bar */}
+
+
+
+ {readerBook?.name} {readerChapter} · {flipPage + 1} / {flipTotalPages}
+
+
+
+
+ )}
{/* Draw mode — Bible text on left, notebook canvas on right */}
{readerDrawMode && (