Load translation notes by visible chapter
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "libre-study",
|
"name": "libre-study",
|
||||||
"version": "0.1.19",
|
"version": "0.1.20",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "libre-study",
|
"name": "libre-study",
|
||||||
"version": "0.1.19",
|
"version": "0.1.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.11.1",
|
"@tauri-apps/api": "^2.11.1",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "libre-study",
|
"name": "libre-study",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.19",
|
"version": "0.1.20",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 127.0.0.1",
|
"dev": "vite --host 127.0.0.1",
|
||||||
|
|||||||
Generated
+1
-1
@@ -77,7 +77,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "0.1.19"
|
version = "0.1.20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "0.1.19"
|
version = "0.1.20"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
license = ""
|
license = ""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
"productName": "Libre Study",
|
"productName": "Libre Study",
|
||||||
"version": "0.1.19",
|
"version": "0.1.20",
|
||||||
"identifier": "org.librestudy.desktop",
|
"identifier": "org.librestudy.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
|
|||||||
+53
@@ -4,6 +4,7 @@ import {
|
|||||||
getCommentaryForVerse,
|
getCommentaryForVerse,
|
||||||
getDictionaryEntriesForTerms,
|
getDictionaryEntriesForTerms,
|
||||||
getLibraryState,
|
getLibraryState,
|
||||||
|
getTranslationNotesForVerses,
|
||||||
getTranslations,
|
getTranslations,
|
||||||
searchVerses,
|
searchVerses,
|
||||||
} from './studyClient'
|
} from './studyClient'
|
||||||
@@ -56,6 +57,16 @@ function reference(verse: Verse) {
|
|||||||
return `${verse.book} ${verse.chapter}:${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 {
|
function createSession(title = 'Study Session', baseTranslation = defaultTranslationId): StudySession {
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
const id =
|
const id =
|
||||||
@@ -268,6 +279,7 @@ function App() {
|
|||||||
const pageMotionTimeout = useRef<number | null>(null)
|
const pageMotionTimeout = useRef<number | null>(null)
|
||||||
const notesDragOpenTimeout = useRef<number | null>(null)
|
const notesDragOpenTimeout = useRef<number | null>(null)
|
||||||
const readerRef = useRef<HTMLElement | null>(null)
|
const readerRef = useRef<HTMLElement | null>(null)
|
||||||
|
const hydratedNoteChapters = useRef(new Set<string>())
|
||||||
|
|
||||||
async function loadLibrary(translationId = activeSession?.baseTranslation ?? defaultTranslationId) {
|
async function loadLibrary(translationId = activeSession?.baseTranslation ?? defaultTranslationId) {
|
||||||
setError(null)
|
setError(null)
|
||||||
@@ -643,6 +655,47 @@ function App() {
|
|||||||
.slice(start, start + pageSize)
|
.slice(start, start + pageSize)
|
||||||
.filter((verse) => verse.book === book)
|
.filter((verse) => verse.book === book)
|
||||||
}, [libraryVerses, pageStartId, selectedVerse, verses])
|
}, [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
|
const pageRange = pageVerses.length
|
||||||
? `${reference(pageVerses[0])} - ${reference(pageVerses[pageVerses.length - 1])}`
|
? `${reference(pageVerses[0])} - ${reference(pageVerses[pageVerses.length - 1])}`
|
||||||
: 'No page loaded'
|
: 'No page loaded'
|
||||||
|
|||||||
+81
-10
@@ -118,6 +118,8 @@ const browserLibraryCache = new Map<string, Promise<Verse[]>>()
|
|||||||
const browserStrongDataCache = new Map<string, Promise<BrowserStrongData>>()
|
const browserStrongDataCache = new Map<string, Promise<BrowserStrongData>>()
|
||||||
const browserFootnotesCache = new Map<string, Promise<Map<string, Footnote[]>>>()
|
const browserFootnotesCache = new Map<string, Promise<Map<string, Footnote[]>>>()
|
||||||
const browserStudyNotesCache = new Map<string, Promise<Map<string, Footnote[]>>>()
|
const browserStudyNotesCache = new Map<string, Promise<Map<string, Footnote[]>>>()
|
||||||
|
const browserChapterFootnotesCache = new Map<string, Promise<Map<string, Footnote[]>>>()
|
||||||
|
const browserChapterStudyNotesCache = new Map<string, Promise<Map<string, Footnote[]>>>()
|
||||||
let browserCommentaryCache: Promise<Map<string, CommentaryEntry[]>> | null = null
|
let browserCommentaryCache: Promise<Map<string, CommentaryEntry[]>> | null = null
|
||||||
let browserDictionaryCache: Promise<Map<string, DictionaryEntry>> | null = null
|
let browserDictionaryCache: Promise<Map<string, DictionaryEntry>> | null = null
|
||||||
const browserVerseSourceIds = new Map<number, string>()
|
const browserVerseSourceIds = new Map<number, string>()
|
||||||
@@ -273,20 +275,12 @@ async function loadBrowserVersesForTranslation(translationId = defaultTranslatio
|
|||||||
console.warn('Unable to load browser Strong data.', error)
|
console.warn('Unable to load browser Strong data.', error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
const footnotes = await loadBrowserFootnotes(translationId).catch((error) => {
|
|
||||||
console.warn('Unable to load browser footnotes.', error)
|
|
||||||
return new Map<string, Footnote[]>()
|
|
||||||
})
|
|
||||||
const studyNotes = await loadBrowserStudyNotes(translationId).catch((error) => {
|
|
||||||
console.warn('Unable to load browser study notes.', error)
|
|
||||||
return new Map<string, Footnote[]>()
|
|
||||||
})
|
|
||||||
const tagsByVerse = readBrowserTags()
|
const tagsByVerse = readBrowserTags()
|
||||||
return verses.map((verse) => ({
|
return verses.map((verse) => ({
|
||||||
...verse,
|
...verse,
|
||||||
strongs: strongData?.linksByVerse.get(browserVerseKey(verse)) ?? verse.strongs,
|
strongs: strongData?.linksByVerse.get(browserVerseKey(verse)) ?? verse.strongs,
|
||||||
footnotes: footnotes.get(browserVerseKey(verse)) ?? verse.footnotes ?? [],
|
footnotes: verse.footnotes ?? [],
|
||||||
studyNotes: studyNotes.get(browserVerseKey(verse)) ?? verse.studyNotes ?? [],
|
studyNotes: verse.studyNotes ?? [],
|
||||||
tags: tagsByVerse[verse.id] ?? [],
|
tags: tagsByVerse[verse.id] ?? [],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -329,6 +323,37 @@ export async function getLibraryState(translationId = defaultTranslationId): Pro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getTranslationNotesForVerses(
|
||||||
|
translationId: string,
|
||||||
|
verses: Verse[],
|
||||||
|
): Promise<Map<string, { footnotes: Footnote[]; studyNotes: Footnote[] }>> {
|
||||||
|
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<string, { footnotes: Footnote[]; studyNotes: Footnote[] }>()
|
||||||
|
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<Verse[]> {
|
export async function searchVerses(query: string, translationId = defaultTranslationId): Promise<Verse[]> {
|
||||||
if (inTauri()) {
|
if (inTauri()) {
|
||||||
return invoke<Verse[]>('search_verses', { query })
|
return invoke<Verse[]>('search_verses', { query })
|
||||||
@@ -539,6 +564,47 @@ async function loadLibreBibleDevStudyNotes(translationId = defaultTranslationId)
|
|||||||
return mapImportedNotes(importedFootnotes)
|
return mapImportedNotes(importedFootnotes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadBrowserChapterNotes(
|
||||||
|
translationId: string,
|
||||||
|
chapterKey: string,
|
||||||
|
kind: 'footnotes' | 'study-notes',
|
||||||
|
): Promise<Map<string, Footnote[]>> {
|
||||||
|
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<string, Footnote[]>()
|
||||||
|
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<Map<string, Footnote[]>> {
|
||||||
|
const importedNotes = await fetchJsonl<ImportedFootnote>(
|
||||||
|
`${libreBibleDevRoot}/${translationId}/notes-by-chapter/${chapterKey}.${kind}.jsonl`,
|
||||||
|
)
|
||||||
|
|
||||||
|
return mapImportedNotes(importedNotes)
|
||||||
|
}
|
||||||
|
|
||||||
function mapImportedNotes(importedFootnotes: ImportedFootnote[]): Map<string, Footnote[]> {
|
function mapImportedNotes(importedFootnotes: ImportedFootnote[]): Map<string, Footnote[]> {
|
||||||
const footnotes = new Map<string, Footnote[]>()
|
const footnotes = new Map<string, Footnote[]>()
|
||||||
|
|
||||||
@@ -619,6 +685,11 @@ function strongLanguage(strong: string): 'Hebrew' | 'Greek' {
|
|||||||
return strong.startsWith('H') ? '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) {
|
function browserVerseKey(verse: Verse) {
|
||||||
return browserVerseSourceIds.get(verse.id) ?? ''
|
return browserVerseSourceIds.get(verse.id) ?? ''
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user