Version history, notes on highlights and bookmarks, "you've studied this", prayer journal
Build and Push Docker Image / build-and-push (push) Successful in 5m13s

- Earlier versions of a study: the server keeps compressed copies (about one per ten minutes of
  editing, the latest 30); Export shows what differs and puts one back (the replaced copy is kept)
- Notes on highlights and bookmarks: synced, merged without loss, in exports and backups
- Reader verse panel lists your studies and chunks that cover the verse, and opens them
- Prayer journal: requests, who for, linked verses, days prayed, answers; synced, in backups
  and the account export; daily prayer reminder through ntfy
- TODO.md lists the ideas not built yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-06 10:45:32 -04:00
1 parent 54cb1c217d
commit 207bfcb919
45 files changed
+1669 -75

No files matched your search

+39 -6
View File
@@ -13,12 +13,13 @@ import { useAuth } from './hooks/useAuth.js';
import { useBibleReader } from './hooks/useBibleReader.js';
import { useProjectLibrary } from './hooks/useProjectLibrary.js';
import { useHistoryNav } from './hooks/useHistoryNav.js';
import { usePrayers } from './hooks/usePrayers.js';
import { useReadingHistory } from './hooks/useReadingHistory.js';
import { useReadingPlans } from './hooks/useReadingPlans.js';
import { useSharedView } from './hooks/useSharedView.js';
import HomePage from './pages/HomePage.jsx';
import { themeVars } from './components/reader/readerThemes.js';
import { joinStudy, loadRemoteProject, saveRemoteProject } from './syncService.js';
import { joinStudy, loadRemoteProject, restoreStudyVersion, saveRemoteProject } from './syncService.js';
import { getSyncBase, setSyncBase } from './utils/syncBase.js';
import {
bookOptions, buildExportHtml, loadProjectIndex, makeId, parseBibleChapter, switchStorageUser,
@@ -42,6 +43,7 @@ const ScriptPage = lazy(() => import('./pages/ScriptPage.jsx'));
const ExportPage = lazy(() => import('./pages/ExportPage.jsx'));
const PronunciationPage = lazy(() => import('./pages/PronunciationPage.jsx'));
const ReadingPlansPage = lazy(() => import('./pages/ReadingPlansPage.jsx'));
const PrayerPage = lazy(() => import('./pages/PrayerPage.jsx'));
// ---------------------------------------------------------------------------
// App component
@@ -59,7 +61,7 @@ function savedStartPage() {
const AppInner = () => {
const { toast, setTheme: setToastTheme } = useToast();
const [project, setProject] = useState(null);
// 'home' | 'setup' | 'study' | 'outline' | 'script' | 'export' | 'reader' | 'import' | 'pronunciation' | 'settings' | 'admin' | 'reading-plans'
// 'home' | 'setup' | 'study' | 'outline' | 'script' | 'export' | 'reader' | 'import' | 'pronunciation' | 'settings' | 'admin' | 'reading-plans' | 'prayer'
const [currentPage, setCurrentPage] = useState(savedStartPage);
const currentPageRef = useRef(currentPage);
currentPageRef.current = currentPage;
@@ -75,6 +77,8 @@ const AppInner = () => {
const [readerPlan, setReaderPlan] = useState(null);
const readerPlanProgress = readerPlan ? readingPlans.planProgress.find((p) => p.id === readerPlan.progressId) ?? null : null;
const reader = useBibleReader(currentPage, readerPlanProgress?.translation ?? null);
const prayerJournal = usePrayers();
const [prayerDraft, setPrayerDraft] = useState(null); // a verse to start a prayer with (from the Reader)
const readingHistory = useReadingHistory({ active: currentPage === 'reader', book: reader.readerBookAbbrev, chapter: reader.readerChapter });
const audio = useAudio(reader.readerBookAbbrev, reader.readerChapter, reader.readerTranslation);
useEffect(() => { if (currentPage !== 'reader') setReaderPlan(null); }, [currentPage]);
@@ -104,12 +108,14 @@ const AppInner = () => {
reader.pullServerPrefs();
// What you've read, merged with this device's.
readingHistory.pullReadingHistory();
prayerJournal.pullPrayers();
},
onSignOut: () => {
reader.stopMarksSync();
reader.stopPrefsSync();
reader.stopInkSync();
readingHistory.stopReadingHistory();
prayerJournal.stopPrayers();
readingPlans.stopReadingPlansSync();
setProjectIndex(switchStorageUser(null));
goStudies();
@@ -161,7 +167,7 @@ const AppInner = () => {
useEffect(() => {
const names = {
home: 'My Studies', setup: 'Chunks', study: 'Study', outline: 'Outline', script: 'Script', export: 'Export',
import: 'Import', pronunciation: 'Pronunciation Guide', settings: 'Settings', admin: 'Admin', 'reading-plans': 'Reading Plans',
import: 'Import', pronunciation: 'Pronunciation Guide', settings: 'Settings', admin: 'Admin', 'reading-plans': 'Reading Plans', prayer: 'Prayer journal',
};
let page = names[currentPage] ?? '';
if (currentPage === 'reader') {
@@ -251,11 +257,11 @@ const AppInner = () => {
const flushPendingSave = () => {
const pending = pendingSaveRef.current;
if (!pending) return;
if (!pending) return Promise.resolve();
window.clearTimeout(saveTimerRef.current);
pendingSaveRef.current = null;
savedSignatureRef.current = { id: pending.project.id, signature: pending.signature };
commitSave(pending.project);
return commitSave(pending.project);
};
const flushRef = useRef(flushPendingSave);
flushRef.current = flushPendingSave;
@@ -305,6 +311,27 @@ const AppInner = () => {
toast('Switched to the version from your other device');
};
// Puts an earlier version of the open study back. What you just typed is saved first (so it is in the list too),
// then the server swaps the versions, and this device takes the restored one.
const restoreEarlierVersion = async (versionId) => {
if (!project) return false;
await flushPendingSave();
const result = await restoreStudyVersion(project.id, versionId);
if (!result.ok) return false;
window.clearTimeout(saveTimerRef.current);
pendingSaveRef.current = null;
const restored = result.data;
try { saveProjectToStorage(restored); } catch { /* storage full: the account has it */ }
unsyncedStudies.remove(restored.id);
setSyncBase(restored.id, restored.lastEdited);
savedSignatureRef.current = { id: restored.id, signature: signatureOf(restored) };
setSyncConflict(null);
setProject(restored);
setProjectIndex(loadProjectIndex());
toast('Earlier version put back. What you had before is still in the list.');
return true;
};
// Third way out of a conflict: combine both copies chunk by chunk, then save the result as the new version.
const mergeBothVersions = async () => {
if (!project || !syncConflict) return;
@@ -342,6 +369,9 @@ const AppInner = () => {
const openBibleReader = () => setCurrentPage('reader');
const openReadingPlans = () => setCurrentPage('reading-plans');
const openPrayer = () => setCurrentPage('prayer');
// "Pray about this verse": opens the journal with the verse filled in
const startPrayerFor = (label) => { setPrayerDraft({ label }); setCurrentPage('prayer'); };
// Jumps straight into a specific chapter and opens the reader (no plan context).
const readReadingPlanDay = (bookAbbrev, chapter) => {
@@ -477,7 +507,7 @@ const AppInner = () => {
saveStatus, setSaveStatus,
syncStatus,
retrySync,
syncConflict, keepMyVersion, useServerVersion, mergeBothVersions,
syncConflict, keepMyVersion, useServerVersion, mergeBothVersions, restoreEarlierVersion,
// Home
projectIndex,
...library,
@@ -502,6 +532,8 @@ const AppInner = () => {
// Reader
...reader,
readingHistory: readingHistory.readingHistory,
...prayerJournal,
prayerDraft, clearPrayerDraft: () => setPrayerDraft(null), startPrayerFor, openPrayer,
readerPlan, setReaderPlan,
readerPlanActive: !!readerPlanProgress,
openPlanReading,
@@ -594,6 +626,7 @@ function AppRouter() {
if (currentPage === 'admin') return <AdminPage />;
if (currentPage === 'settings') return <SettingsPage />;
if (currentPage === 'reading-plans') return <ReadingPlansPage />;
if (currentPage === 'prayer') return <PrayerPage />;
if (currentPage === 'reader') return <ReaderPage />;
if (currentPage === 'import') return <ImportPage />;
if (currentPage === 'study') return <StudyPage />;