From 41ca2b850c10a9f2d155079592d1ecbd38009365 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 12 Jul 2026 11:47:31 -0500 Subject: [PATCH] Load translation notes by visible chapter --- package-lock.json | 4 +- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- src/App.tsx | 53 +++++++++++++++++++++++ src/studyClient.ts | 91 ++++++++++++++++++++++++++++++++++----- 7 files changed, 140 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 308aa6c..7ce4f82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "libre-study", - "version": "0.1.19", + "version": "0.1.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "libre-study", - "version": "0.1.19", + "version": "0.1.20", "dependencies": { "@tauri-apps/api": "^2.11.1", "react": "^19.2.7", diff --git a/package.json b/package.json index 3cf4895..8dc2957 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "libre-study", "private": true, - "version": "0.1.19", + "version": "0.1.20", "type": "module", "scripts": { "dev": "vite --host 127.0.0.1", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 42fa6f3..35fd430 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -77,7 +77,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "app" -version = "0.1.19" +version = "0.1.20" dependencies = [ "log", "rusqlite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3f42959..c02475b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "app" -version = "0.1.19" +version = "0.1.20" description = "A Tauri App" authors = ["you"] license = "" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index d7609a1..e81b9bb 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "Libre Study", - "version": "0.1.19", + "version": "0.1.20", "identifier": "org.librestudy.desktop", "build": { "frontendDist": "../dist", diff --git a/src/App.tsx b/src/App.tsx index 8cb8ea8..b181828 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import { getCommentaryForVerse, getDictionaryEntriesForTerms, getLibraryState, + getTranslationNotesForVerses, getTranslations, searchVerses, } from './studyClient' @@ -56,6 +57,16 @@ function reference(verse: Verse) { return `${verse.book} ${verse.chapter}:${verse.verse}` } +function replaceChangedVerses(verses: Verse[], hydrate: (verse: Verse) => Verse) { + let changed = false + const next = verses.map((verse) => { + const hydrated = hydrate(verse) + if (hydrated !== verse) changed = true + return hydrated + }) + return changed ? next : verses +} + function createSession(title = 'Study Session', baseTranslation = defaultTranslationId): StudySession { const now = new Date().toISOString() const id = @@ -268,6 +279,7 @@ function App() { const pageMotionTimeout = useRef(null) const notesDragOpenTimeout = useRef(null) const readerRef = useRef(null) + const hydratedNoteChapters = useRef(new Set()) async function loadLibrary(translationId = activeSession?.baseTranslation ?? defaultTranslationId) { setError(null) @@ -643,6 +655,47 @@ function App() { .slice(start, start + pageSize) .filter((verse) => verse.book === book) }, [libraryVerses, pageStartId, selectedVerse, verses]) + + useEffect(() => { + if (!activeSession?.baseTranslation || pageVerses.length === 0) return + const chapterKeys = [ + ...new Set( + pageVerses.map( + (verse) => `${activeSession.baseTranslation}:${verse.book}:${verse.chapter}`, + ), + ), + ] + const unloadedChapterKeys = chapterKeys.filter( + (chapterKey) => !hydratedNoteChapters.current.has(chapterKey), + ) + if (unloadedChapterKeys.length === 0) return + for (const chapterKey of unloadedChapterKeys) hydratedNoteChapters.current.add(chapterKey) + + let cancelled = false + getTranslationNotesForVerses(activeSession.baseTranslation, pageVerses) + .then((notesByVerse) => { + if (cancelled || notesByVerse.size === 0) return + const hydrate = (verse: Verse) => { + const notes = notesByVerse.get(String(verse.id)) + if (!notes) return verse + if ( + (verse.footnotes?.length ?? 0) === notes.footnotes.length && + (verse.studyNotes?.length ?? 0) === notes.studyNotes.length + ) { + return verse + } + return { ...verse, footnotes: notes.footnotes, studyNotes: notes.studyNotes } + } + setLibraryVerses((current) => replaceChangedVerses(current, hydrate)) + setVerses((current) => replaceChangedVerses(current, hydrate)) + }) + .catch((reason) => setError(String(reason))) + + return () => { + cancelled = true + } + }, [activeSession?.baseTranslation, pageVerses]) + const pageRange = pageVerses.length ? `${reference(pageVerses[0])} - ${reference(pageVerses[pageVerses.length - 1])}` : 'No page loaded' diff --git a/src/studyClient.ts b/src/studyClient.ts index b94e7c3..4b402e7 100644 --- a/src/studyClient.ts +++ b/src/studyClient.ts @@ -118,6 +118,8 @@ const browserLibraryCache = new Map>() const browserStrongDataCache = new Map>() const browserFootnotesCache = new Map>>() const browserStudyNotesCache = new Map>>() +const browserChapterFootnotesCache = new Map>>() +const browserChapterStudyNotesCache = new Map>>() let browserCommentaryCache: Promise> | null = null let browserDictionaryCache: Promise> | null = null const browserVerseSourceIds = new Map() @@ -273,20 +275,12 @@ async function loadBrowserVersesForTranslation(translationId = defaultTranslatio console.warn('Unable to load browser Strong data.', error) return null }) - const footnotes = await loadBrowserFootnotes(translationId).catch((error) => { - console.warn('Unable to load browser footnotes.', error) - return new Map() - }) - const studyNotes = await loadBrowserStudyNotes(translationId).catch((error) => { - console.warn('Unable to load browser study notes.', error) - return new Map() - }) const tagsByVerse = readBrowserTags() return verses.map((verse) => ({ ...verse, strongs: strongData?.linksByVerse.get(browserVerseKey(verse)) ?? verse.strongs, - footnotes: footnotes.get(browserVerseKey(verse)) ?? verse.footnotes ?? [], - studyNotes: studyNotes.get(browserVerseKey(verse)) ?? verse.studyNotes ?? [], + footnotes: verse.footnotes ?? [], + studyNotes: verse.studyNotes ?? [], tags: tagsByVerse[verse.id] ?? [], })) } @@ -329,6 +323,37 @@ export async function getLibraryState(translationId = defaultTranslationId): Pro } } +export async function getTranslationNotesForVerses( + translationId: string, + verses: Verse[], +): Promise> { + if (inTauri() || verses.length === 0) return new Map() + + const chapterKeys = [ + ...new Set( + verses + .map((verse) => `${bookIdFromVerse(verse)}.${verse.chapter}`) + .filter((key) => !key.startsWith('unknown.')), + ), + ] + + const [footnoteMaps, studyNoteMaps] = await Promise.all([ + Promise.all(chapterKeys.map((key) => loadBrowserChapterNotes(translationId, key, 'footnotes'))), + Promise.all(chapterKeys.map((key) => loadBrowserChapterNotes(translationId, key, 'study-notes'))), + ]) + + const notesByVerse = new Map() + for (const verse of verses) { + const verseKey = browserVerseKey(verse) + notesByVerse.set(String(verse.id), { + footnotes: footnoteMaps.flatMap((map) => map.get(verseKey) ?? []), + studyNotes: studyNoteMaps.flatMap((map) => map.get(verseKey) ?? []), + }) + } + + return notesByVerse +} + export async function searchVerses(query: string, translationId = defaultTranslationId): Promise { if (inTauri()) { return invoke('search_verses', { query }) @@ -539,6 +564,47 @@ async function loadLibreBibleDevStudyNotes(translationId = defaultTranslationId) return mapImportedNotes(importedFootnotes) } +async function loadBrowserChapterNotes( + translationId: string, + chapterKey: string, + kind: 'footnotes' | 'study-notes', +): Promise> { + const cacheKey = `${translationId}:${chapterKey}:${kind}` + const cache = + kind === 'footnotes' ? browserChapterFootnotesCache : browserChapterStudyNotesCache + + if (!cache.has(cacheKey)) { + cache.set( + cacheKey, + loadLibreBibleDevChapterNotes(translationId, chapterKey, kind).catch(async () => { + const fullNotes = + kind === 'footnotes' + ? await loadBrowserFootnotes(translationId) + : await loadBrowserStudyNotes(translationId) + const filtered = new Map() + for (const [verseId, notes] of fullNotes) { + if (verseId.startsWith(`${chapterKey}.`)) filtered.set(verseId, notes) + } + return filtered + }), + ) + } + + return cache.get(cacheKey)! +} + +async function loadLibreBibleDevChapterNotes( + translationId: string, + chapterKey: string, + kind: 'footnotes' | 'study-notes', +): Promise> { + const importedNotes = await fetchJsonl( + `${libreBibleDevRoot}/${translationId}/notes-by-chapter/${chapterKey}.${kind}.jsonl`, + ) + + return mapImportedNotes(importedNotes) +} + function mapImportedNotes(importedFootnotes: ImportedFootnote[]): Map { const footnotes = new Map() @@ -619,6 +685,11 @@ function strongLanguage(strong: string): 'Hebrew' | 'Greek' { return strong.startsWith('H') ? 'Hebrew' : 'Greek' } +function bookIdFromVerse(verse: Verse): string { + const sourceId = browserVerseSourceIds.get(verse.id) + return sourceId?.split('.')[0] ?? 'unknown' +} + function browserVerseKey(verse: Verse) { return browserVerseSourceIds.get(verse.id) ?? '' }