Load translations from LibreBible catalog

This commit is contained in:
2026-07-12 11:11:54 -05:00
parent 735e247477
commit b91912649c
8 changed files with 305 additions and 61 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "libre-study", "name": "libre-study",
"version": "0.1.18", "version": "0.1.19",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "libre-study", "name": "libre-study",
"version": "0.1.18", "version": "0.1.19",
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.11.1", "@tauri-apps/api": "^2.11.1",
"react": "^19.2.7", "react": "^19.2.7",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "libre-study", "name": "libre-study",
"private": true, "private": true,
"version": "0.1.18", "version": "0.1.19",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host 127.0.0.1", "dev": "vite --host 127.0.0.1",
+1 -1
View File
@@ -77,7 +77,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
[[package]] [[package]]
name = "app" name = "app"
version = "0.1.18" version = "0.1.19"
dependencies = [ dependencies = [
"log", "log",
"rusqlite", "rusqlite",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "app" name = "app"
version = "0.1.18" version = "0.1.19"
description = "A Tauri App" description = "A Tauri App"
authors = ["you"] authors = ["you"]
license = "" license = ""
+1 -1
View File
@@ -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.18", "version": "0.1.19",
"identifier": "org.librestudy.desktop", "identifier": "org.librestudy.desktop",
"build": { "build": {
"frontendDist": "../dist", "frontendDist": "../dist",
+153 -25
View File
@@ -4,10 +4,11 @@ import {
getCommentaryForVerse, getCommentaryForVerse,
getDictionaryEntriesForTerms, getDictionaryEntriesForTerms,
getLibraryState, getLibraryState,
getTranslations,
searchVerses, searchVerses,
} from './studyClient' } from './studyClient'
import type { DragEvent, MouseEvent as ReactMouseEvent, ReactNode } from 'react' import type { DragEvent, MouseEvent as ReactMouseEvent, ReactNode } from 'react'
import type { CommentaryEntry, DictionaryEntry, Verse } from './types' import type { CommentaryEntry, DictionaryEntry, TranslationResource, Verse } from './types'
import './App.css' import './App.css'
const tagColors = ['#2f6f5e', '#735c0f', '#8a3ffc', '#b42318', '#245ba7'] const tagColors = ['#2f6f5e', '#735c0f', '#8a3ffc', '#b42318', '#245ba7']
@@ -16,6 +17,7 @@ const sessionStorageKey = 'libre-study.sessions.v1'
const activeSessionStorageKey = 'libre-study.active-session.v1' const activeSessionStorageKey = 'libre-study.active-session.v1'
const studySettingsStorageKey = 'libre-study.study-settings.v1' const studySettingsStorageKey = 'libre-study.study-settings.v1'
const timelineLimit = 200 const timelineLimit = 200
const defaultTranslationId = 'kjv-eng-kjv2006'
type ViewMode = 'page' | 'study' | 'notes' | 'compare' type ViewMode = 'page' | 'study' | 'notes' | 'compare'
type StrongDisplayMode = 'off' | 'highlight' | 'numbers' type StrongDisplayMode = 'off' | 'highlight' | 'numbers'
type PageMotion = 'idle' | 'turn-forward' | 'turn-back' type PageMotion = 'idle' | 'turn-forward' | 'turn-back'
@@ -54,7 +56,7 @@ function reference(verse: Verse) {
return `${verse.book} ${verse.chapter}:${verse.verse}` return `${verse.book} ${verse.chapter}:${verse.verse}`
} }
function createSession(title = 'Study Session'): StudySession { function createSession(title = 'Study Session', baseTranslation = defaultTranslationId): StudySession {
const now = new Date().toISOString() const now = new Date().toISOString()
const id = const id =
typeof crypto !== 'undefined' && 'randomUUID' in crypto typeof crypto !== 'undefined' && 'randomUUID' in crypto
@@ -64,7 +66,7 @@ function createSession(title = 'Study Session'): StudySession {
return { return {
id, id,
title, title,
baseTranslation: 'kjv-eng-kjv2006', baseTranslation,
footnotesEnabled: true, footnotesEnabled: true,
strongsEnabled: true, strongsEnabled: true,
commentaryEnabled: true, commentaryEnabled: true,
@@ -227,6 +229,7 @@ function App() {
const [themeMode, setThemeMode] = useState<ThemeMode>('light') const [themeMode, setThemeMode] = useState<ThemeMode>('light')
const [libraryVerses, setLibraryVerses] = useState<Verse[]>([]) const [libraryVerses, setLibraryVerses] = useState<Verse[]>([])
const [verses, setVerses] = useState<Verse[]>([]) const [verses, setVerses] = useState<Verse[]>([])
const [translations, setTranslations] = useState<TranslationResource[]>([])
const [selectedId, setSelectedId] = useState<number | null>(null) const [selectedId, setSelectedId] = useState<number | null>(null)
const [pageStartId, setPageStartId] = useState<number | null>(null) const [pageStartId, setPageStartId] = useState<number | null>(null)
const [selectedStrong, setSelectedStrong] = useState<string | null>(null) const [selectedStrong, setSelectedStrong] = useState<string | null>(null)
@@ -236,6 +239,7 @@ function App() {
const [sessions, setSessions] = useState<StudySession[]>([]) const [sessions, setSessions] = useState<StudySession[]>([])
const [activeSessionId, setActiveSessionId] = useState('') const [activeSessionId, setActiveSessionId] = useState('')
const [newSessionTitle, setNewSessionTitle] = useState('New KJV Study') const [newSessionTitle, setNewSessionTitle] = useState('New KJV Study')
const [newSessionBaseTranslation, setNewSessionBaseTranslation] = useState(defaultTranslationId)
const [newSessionFootnotes, setNewSessionFootnotes] = useState(true) const [newSessionFootnotes, setNewSessionFootnotes] = useState(true)
const [newSessionStrongs, setNewSessionStrongs] = useState(true) const [newSessionStrongs, setNewSessionStrongs] = useState(true)
const [newSessionCommentary, setNewSessionCommentary] = useState(true) const [newSessionCommentary, setNewSessionCommentary] = useState(true)
@@ -246,6 +250,7 @@ function App() {
const [originalCollapsed, setOriginalCollapsed] = useState(false) const [originalCollapsed, setOriginalCollapsed] = useState(false)
const [linkedCollapsed, setLinkedCollapsed] = useState(false) const [linkedCollapsed, setLinkedCollapsed] = useState(false)
const [definitionCollapsed, setDefinitionCollapsed] = useState(false) const [definitionCollapsed, setDefinitionCollapsed] = useState(false)
const [studyNotesCollapsed, setStudyNotesCollapsed] = useState(false)
const [commentaryCollapsed, setCommentaryCollapsed] = useState(false) const [commentaryCollapsed, setCommentaryCollapsed] = useState(false)
const [dictionaryCollapsed, setDictionaryCollapsed] = useState(false) const [dictionaryCollapsed, setDictionaryCollapsed] = useState(false)
const [showOriginalLine, setShowOriginalLine] = useState(true) const [showOriginalLine, setShowOriginalLine] = useState(true)
@@ -264,9 +269,9 @@ function App() {
const notesDragOpenTimeout = useRef<number | null>(null) const notesDragOpenTimeout = useRef<number | null>(null)
const readerRef = useRef<HTMLElement | null>(null) const readerRef = useRef<HTMLElement | null>(null)
async function loadLibrary() { async function loadLibrary(translationId = activeSession?.baseTranslation ?? defaultTranslationId) {
setError(null) setError(null)
const state = await getLibraryState() const state = await getLibraryState(translationId)
setLibraryVerses(state.verses) setLibraryVerses(state.verses)
setVerses(state.verses) setVerses(state.verses)
const firstId = state.verses[0]?.id ?? null const firstId = state.verses[0]?.id ?? null
@@ -352,7 +357,7 @@ function App() {
setError(null) setError(null)
const trimmed = nextQuery.trim() const trimmed = nextQuery.trim()
setSelectedStrong(/^[gh]\d+$/i.test(trimmed) ? trimmed.toUpperCase() : null) setSelectedStrong(/^[gh]\d+$/i.test(trimmed) ? trimmed.toUpperCase() : null)
const results = await searchVerses(nextQuery) const results = await searchVerses(nextQuery, activeSession?.baseTranslation ?? defaultTranslationId)
setVerses(results) setVerses(results)
const firstResult = results[0] const firstResult = results[0]
setSelectedId(firstResult?.id ?? null) setSelectedId(firstResult?.id ?? null)
@@ -375,7 +380,12 @@ function App() {
if (!selectedId || !label) return if (!selectedId || !label) return
const color = tagColors[Math.abs(label.length) % tagColors.length] const color = tagColors[Math.abs(label.length) % tagColors.length]
const updated = await addTagToVerse(selectedId, label, color) const updated = await addTagToVerse(
selectedId,
label,
color,
activeSession?.baseTranslation ?? defaultTranslationId,
)
setLibraryVerses((current) => setLibraryVerses((current) =>
current.map((verse) => (verse.id === updated.id ? updated : verse)), current.map((verse) => (verse.id === updated.id ? updated : verse)),
@@ -400,6 +410,7 @@ function App() {
originalCollapsed: boolean originalCollapsed: boolean
linkedCollapsed: boolean linkedCollapsed: boolean
definitionCollapsed: boolean definitionCollapsed: boolean
studyNotesCollapsed: boolean
commentaryCollapsed: boolean commentaryCollapsed: boolean
dictionaryCollapsed: boolean dictionaryCollapsed: boolean
showOriginalLine: boolean showOriginalLine: boolean
@@ -416,6 +427,7 @@ function App() {
setOriginalCollapsed(parsed.originalCollapsed ?? false) setOriginalCollapsed(parsed.originalCollapsed ?? false)
setLinkedCollapsed(parsed.linkedCollapsed ?? false) setLinkedCollapsed(parsed.linkedCollapsed ?? false)
setDefinitionCollapsed(parsed.definitionCollapsed ?? false) setDefinitionCollapsed(parsed.definitionCollapsed ?? false)
setStudyNotesCollapsed(parsed.studyNotesCollapsed ?? false)
setCommentaryCollapsed(parsed.commentaryCollapsed ?? false) setCommentaryCollapsed(parsed.commentaryCollapsed ?? false)
setDictionaryCollapsed(parsed.dictionaryCollapsed ?? false) setDictionaryCollapsed(parsed.dictionaryCollapsed ?? false)
setShowOriginalLine(parsed.showOriginalLine ?? true) setShowOriginalLine(parsed.showOriginalLine ?? true)
@@ -447,7 +459,7 @@ function App() {
nextSessions = nextSessions.map((session) => ({ nextSessions = nextSessions.map((session) => ({
...createSession(session.title), ...createSession(session.title),
...session, ...session,
baseTranslation: session.baseTranslation ?? 'kjv-eng-kjv2006', baseTranslation: session.baseTranslation ?? defaultTranslationId,
footnotesEnabled: session.footnotesEnabled ?? true, footnotesEnabled: session.footnotesEnabled ?? true,
strongsEnabled: session.strongsEnabled ?? true, strongsEnabled: session.strongsEnabled ?? true,
commentaryEnabled: session.commentaryEnabled ?? true, commentaryEnabled: session.commentaryEnabled ?? true,
@@ -455,14 +467,17 @@ function App() {
timelineEnabled: session.timelineEnabled ?? true, timelineEnabled: session.timelineEnabled ?? true,
})) }))
setSessions(nextSessions) const selectedSessionId = nextSessions.some((session) => session.id === savedActiveSessionId)
setActiveSessionId(
nextSessions.some((session) => session.id === savedActiveSessionId)
? savedActiveSessionId ?? nextSessions[0].id ? savedActiveSessionId ?? nextSessions[0].id
: nextSessions[0].id, : nextSessions[0].id
) const selectedSession =
nextSessions.find((session) => session.id === selectedSessionId) ?? nextSessions[0]
loadLibrary() setSessions(nextSessions)
setActiveSessionId(selectedSessionId)
getTranslations().then(setTranslations).catch((reason) => setError(String(reason)))
loadLibrary(selectedSession.baseTranslation)
.catch((reason) => setError(String(reason))) .catch((reason) => setError(String(reason)))
.finally(() => setLoading(false)) .finally(() => setLoading(false))
}, []) }, [])
@@ -488,6 +503,7 @@ function App() {
originalCollapsed, originalCollapsed,
linkedCollapsed, linkedCollapsed,
definitionCollapsed, definitionCollapsed,
studyNotesCollapsed,
commentaryCollapsed, commentaryCollapsed,
dictionaryCollapsed, dictionaryCollapsed,
showOriginalLine, showOriginalLine,
@@ -515,6 +531,7 @@ function App() {
showLinkedLine, showLinkedLine,
showOriginalLine, showOriginalLine,
splitPercent, splitPercent,
studyNotesCollapsed,
themeMode, themeMode,
]) ])
@@ -527,6 +544,7 @@ function App() {
setViewMode(activeSession.viewMode) setViewMode(activeSession.viewMode)
setStrongDisplay(activeSession.strongDisplay) setStrongDisplay(activeSession.strongDisplay)
setNoteDraft(activeSession.noteDraft) setNoteDraft(activeSession.noteDraft)
loadLibrary(activeSession.baseTranslation).catch((reason) => setError(String(reason)))
setSelectedId((current) => activeSession.selectedId ?? current) setSelectedId((current) => activeSession.selectedId ?? current)
setPageStartId((current) => activeSession.pageStartId ?? current) setPageStartId((current) => activeSession.pageStartId ?? current)
setSelectedStrong(null) setSelectedStrong(null)
@@ -550,6 +568,12 @@ function App() {
() => sessions.find((session) => session.id === activeSessionId) ?? sessions[0], () => sessions.find((session) => session.id === activeSessionId) ?? sessions[0],
[activeSessionId, sessions], [activeSessionId, sessions],
) )
const activeTranslation = useMemo(
() =>
translations.find((translation) => translation.id === activeSession?.baseTranslation) ??
translations.find((translation) => translation.id === defaultTranslationId),
[activeSession?.baseTranslation, translations],
)
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
@@ -623,6 +647,7 @@ function App() {
? `${reference(pageVerses[0])} - ${reference(pageVerses[pageVerses.length - 1])}` ? `${reference(pageVerses[0])} - ${reference(pageVerses[pageVerses.length - 1])}`
: 'No page loaded' : 'No page loaded'
const selectedFootnotes = selectedVerse?.footnotes ?? [] const selectedFootnotes = selectedVerse?.footnotes ?? []
const selectedStudyNotes = selectedVerse?.studyNotes ?? []
const pageFootnotes = useMemo( const pageFootnotes = useMemo(
() => pageVerses.flatMap((verse) => verse.footnotes ?? []), () => pageVerses.flatMap((verse) => verse.footnotes ?? []),
[pageVerses], [pageVerses],
@@ -703,6 +728,38 @@ function App() {
) )
} }
async function changeTranslation(translationId: string) {
const translation =
translations.find((item) => item.id === translationId) ??
translations.find((item) => item.id === defaultTranslationId)
const state = await getLibraryState(translationId)
const firstVerse = state.verses[0] ?? null
setQuery('')
setSelectedStrong(null)
setLibraryVerses(state.verses)
setVerses(state.verses)
setSelectedId(firstVerse?.id ?? null)
setPageStartId(firstVerse?.id ?? null)
patchActiveSession({
baseTranslation: translationId,
selectedId: firstVerse?.id ?? null,
pageStartId: firstVerse?.id ?? null,
})
recordTimeline(
{
type: 'setting',
label: `Translation: ${translation?.abbreviation ?? translationId}`,
detail: translation?.title,
},
{
baseTranslation: translationId,
selectedId: firstVerse?.id ?? null,
pageStartId: firstVerse?.id ?? null,
},
)
}
function shiftPage(direction: -1 | 1) { function shiftPage(direction: -1 | 1) {
if (!selectedVerse) return if (!selectedVerse) return
const currentStartIndex = pageAnchors.findIndex((verse) => verse.id === pageStartId) const currentStartIndex = pageAnchors.findIndex((verse) => verse.id === pageStartId)
@@ -851,7 +908,10 @@ function App() {
function startNewSession() { function startNewSession() {
const nextSession = { const nextSession = {
...createSession(newSessionTitle.trim() || `Study Session ${sessions.length + 1}`), ...createSession(
newSessionTitle.trim() || `Study Session ${sessions.length + 1}`,
newSessionBaseTranslation,
),
footnotesEnabled: newSessionFootnotes, footnotesEnabled: newSessionFootnotes,
strongsEnabled: newSessionStrongs, strongsEnabled: newSessionStrongs,
commentaryEnabled: newSessionCommentary, commentaryEnabled: newSessionCommentary,
@@ -860,6 +920,7 @@ function App() {
} }
setSessions((current) => [nextSession, ...current]) setSessions((current) => [nextSession, ...current])
setActiveSessionId(nextSession.id) setActiveSessionId(nextSession.id)
loadLibrary(nextSession.baseTranslation).catch((reason) => setError(String(reason)))
setSessionModalOpen(false) setSessionModalOpen(false)
} }
@@ -1113,6 +1174,52 @@ function App() {
) )
} }
function renderStudyNotesSection() {
if (activeSession?.footnotesEnabled === false) return null
return (
<article className="resource-card study-notes-card" aria-label="Translation study notes">
<header
className="margin-section-header"
role="button"
tabIndex={0}
onClick={() => {
setStudyNotesCollapsed((current) => !current)
recordDisplaySetting('Toggled translation study notes accordion')
}}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
setStudyNotesCollapsed((current) => !current)
recordDisplaySetting('Toggled translation study notes accordion')
}
}}
>
<button type="button" className="collapse-button">Translation Study Notes</button>
<span>{selectedStudyNotes.length ? `${selectedStudyNotes.length} notes` : 'None'}</span>
</header>
{!studyNotesCollapsed ? (
selectedStudyNotes.length ? (
<div className="translation-note-list">
{selectedStudyNotes.map((note) => (
<p
key={note.id}
draggable
onDragStart={(event) => startTextDrag(event, `${note.reference} ${note.text}`)}
>
<strong>{note.reference}</strong>
<span>{note.text}</span>
</p>
))}
</div>
) : (
<p className="resource-disabled">No translation study notes for the selected verse.</p>
)
) : null}
</article>
)
}
function renderCommentarySection() { function renderCommentarySection() {
if (!showCommentaryPanel) return null if (!showCommentaryPanel) return null
@@ -1248,7 +1355,7 @@ function App() {
) )
} }
function renderBiblePage(label = 'KJV') { function renderBiblePage(label = activeTranslation?.abbreviation ?? 'Bible') {
if (!selectedVerse) return null if (!selectedVerse) return null
return ( return (
<article className="bible-page" aria-label={`${label} Bible page`}> <article className="bible-page" aria-label={`${label} Bible page`}>
@@ -1368,6 +1475,7 @@ function App() {
</div> </div>
)} )}
<div className="margin-resource-grid"> <div className="margin-resource-grid">
{renderStudyNotesSection()}
{renderCommentarySection()} {renderCommentarySection()}
{renderDictionarySection()} {renderDictionarySection()}
</div> </div>
@@ -1543,7 +1651,7 @@ function App() {
<dl className="session-facts"> <dl className="session-facts">
<div> <div>
<dt>Translation</dt> <dt>Translation</dt>
<dd>KJV</dd> <dd>{activeTranslation?.abbreviation ?? activeSession?.baseTranslation ?? 'n/a'}</dd>
</div> </div>
<div> <div>
<dt>Current page</dt> <dt>Current page</dt>
@@ -1613,8 +1721,15 @@ function App() {
</label> </label>
<label> <label>
Base translation Base translation
<select value="kjv-eng-kjv2006" onChange={() => null}> <select
<option value="kjv-eng-kjv2006">King James Version</option> value={newSessionBaseTranslation}
onChange={(event) => setNewSessionBaseTranslation(event.target.value)}
>
{translations.map((translation) => (
<option key={translation.id} value={translation.id}>
{translation.shortTitle}
</option>
))}
</select> </select>
</label> </label>
<div className="resource-toggles"> <div className="resource-toggles">
@@ -1702,7 +1817,11 @@ function App() {
</div> </div>
<div> <div>
<dt>Current Bible package</dt> <dt>Current Bible package</dt>
<dd>King James Version, public domain KJV text package with Strong's-linked study data.</dd> <dd>
{activeTranslation
? `${activeTranslation.title}. ${activeTranslation.licenseName}`
: 'LibreBible translation package loaded from the local catalog.'}
</dd>
</div> </div>
<div> <div>
<dt>Software stack</dt> <dt>Software stack</dt>
@@ -2053,8 +2172,17 @@ function App() {
<div className="page-controls" aria-label="Page selectors"> <div className="page-controls" aria-label="Page selectors">
<label> <label>
Translation Translation
<select value="kjv-eng-kjv2006" onChange={() => null}> <select
<option value="kjv-eng-kjv2006">KJV</option> value={activeSession?.baseTranslation ?? defaultTranslationId}
onChange={(event) =>
changeTranslation(event.target.value).catch((reason) => setError(String(reason)))
}
>
{translations.map((translation) => (
<option key={translation.id} value={translation.id}>
{translation.abbreviation}
</option>
))}
</select> </select>
</label> </label>
<label> <label>
@@ -2129,7 +2257,7 @@ function App() {
id="search" id="search"
value={query} value={query}
onChange={(event) => setQuery(event.target.value)} onChange={(event) => setQuery(event.target.value)}
placeholder='Search KJV, reference, or Strong&apos;s' placeholder={`Search ${activeTranslation?.abbreviation ?? 'Bible'}, reference, or Strong's`}
/> />
<button type="submit">Go</button> <button type="submit">Go</button>
</form> </form>
@@ -2149,7 +2277,7 @@ function App() {
{viewMode === 'compare' ? ( {viewMode === 'compare' ? (
<div className="page-system compare-system" aria-label="Compare translations"> <div className="page-system compare-system" aria-label="Compare translations">
<div className="compare-pages"> <div className="compare-pages">
{renderBiblePage('KJV')} {renderBiblePage()}
<article className="bible-page compare-placeholder" aria-label="Second translation placeholder"> <article className="bible-page compare-placeholder" aria-label="Second translation placeholder">
<header> <header>
<span>Second Translation</span> <span>Second Translation</span>
@@ -2168,7 +2296,7 @@ function App() {
) : ( ) : (
<> <>
<div className={`page-system ${viewMode} ${pageMotion}`}> <div className={`page-system ${viewMode} ${pageMotion}`}>
{renderBiblePage('KJV')} {renderBiblePage()}
{renderFootnoteStrip()} {renderFootnoteStrip()}
{renderPageTurner()} {renderPageTurner()}
{renderBookTabs()} {renderBookTabs()}
+132 -31
View File
@@ -1,5 +1,14 @@
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/core'
import type { CommentaryEntry, DictionaryEntry, Footnote, LibraryState, StrongLink, Tag, Verse } from './types' import type {
CommentaryEntry,
DictionaryEntry,
Footnote,
LibraryState,
StrongLink,
Tag,
TranslationResource,
Verse,
} from './types'
type TauriWindow = Window & type TauriWindow = Window &
typeof globalThis & { typeof globalThis & {
@@ -8,9 +17,11 @@ type TauriWindow = Window &
const browserTagKey = 'libre-study-browser-tags' const browserTagKey = 'libre-study-browser-tags'
const libreBibleDevRoot = '/@fs/W:/libre-bible-data/packages/json' const libreBibleDevRoot = '/@fs/W:/libre-bible-data/packages/json'
const defaultTranslationId = 'kjv-eng-kjv2006'
type ImportedVerse = { type ImportedVerse = {
id: string id: string
translation_id: string
book: string book: string
chapter: number chapter: number
verse: number verse: number
@@ -28,11 +39,28 @@ type ImportedFootnote = {
id: number id: number
verse_id: string verse_id: string
note_index: number note_index: number
note_type?: string
label?: string
caller: string caller: string
reference: string reference: string
text: string text: string
} }
type ImportedCatalog = {
resources: Array<{
id: string
resource_type: string
title: string
short_title?: string
abbreviation?: string
license?: {
name?: string
attribution?: string
restricted_notes?: string
}
}>
}
type ImportedStrongEntry = { type ImportedStrongEntry = {
strongs_number: string strongs_number: string
language: 'hebrew' | 'greek' language: 'hebrew' | 'greek'
@@ -85,9 +113,11 @@ type BrowserStrongData = {
verseIdsByStrong: Map<string, Set<string>> verseIdsByStrong: Map<string, Set<string>>
} }
let browserLibraryCache: Promise<Verse[]> | null = null let browserCatalogCache: Promise<ImportedCatalog> | null = null
let browserStrongDataCache: Promise<BrowserStrongData> | null = null const browserLibraryCache = new Map<string, Promise<Verse[]>>()
let browserFootnotesCache: Promise<Map<string, Footnote[]>> | null = null const browserStrongDataCache = new Map<string, Promise<BrowserStrongData>>()
const browserFootnotesCache = new Map<string, Promise<Map<string, Footnote[]>>>()
const browserStudyNotesCache = 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>()
@@ -230,26 +260,33 @@ function writeBrowserTags(tagsByVerse: Record<number, Tag[]>) {
localStorage.setItem(browserTagKey, JSON.stringify(tagsByVerse)) localStorage.setItem(browserTagKey, JSON.stringify(tagsByVerse))
} }
async function loadBrowserVerses() { async function loadBrowserVersesForTranslation(translationId = defaultTranslationId) {
browserLibraryCache ??= loadLibreBibleDevVerses().catch((error) => { if (!browserLibraryCache.has(translationId)) {
browserLibraryCache.set(translationId, loadLibreBibleDevVerses(translationId).catch((error) => {
console.warn('Falling back to browser seed data.', error) console.warn('Falling back to browser seed data.', error)
return fallbackBrowserVerses return fallbackBrowserVerses
}) }))
}
const verses = await browserLibraryCache const verses = await browserLibraryCache.get(translationId)!
const strongData = await loadBrowserStrongData().catch((error) => { const strongData = await loadBrowserStrongData(translationId).catch((error) => {
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().catch((error) => { const footnotes = await loadBrowserFootnotes(translationId).catch((error) => {
console.warn('Unable to load browser footnotes.', error) console.warn('Unable to load browser footnotes.', error)
return new Map<string, Footnote[]>() 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: footnotes.get(browserVerseKey(verse)) ?? verse.footnotes ?? [],
studyNotes: studyNotes.get(browserVerseKey(verse)) ?? verse.studyNotes ?? [],
tags: tagsByVerse[verse.id] ?? [], tags: tagsByVerse[verse.id] ?? [],
})) }))
} }
@@ -264,24 +301,41 @@ function allBrowserTags() {
return [...unique.values()].sort((a, b) => a.label.localeCompare(b.label)) return [...unique.values()].sort((a, b) => a.label.localeCompare(b.label))
} }
export async function getLibraryState(): Promise<LibraryState> { export async function getTranslations(): Promise<TranslationResource[]> {
if (inTauri()) return []
const catalog = await loadBrowserCatalog()
return catalog.resources
.filter((resource) => resource.resource_type === 'translation')
.map((resource) => ({
id: resource.id,
title: resource.title,
shortTitle: resource.short_title ?? resource.title,
abbreviation: resource.abbreviation ?? resource.short_title ?? resource.title,
licenseName: resource.license?.name ?? '',
attribution: resource.license?.attribution ?? '',
restrictions: resource.license?.restricted_notes ?? '',
}))
}
export async function getLibraryState(translationId = defaultTranslationId): Promise<LibraryState> {
if (inTauri()) { if (inTauri()) {
return invoke<LibraryState>('get_library_state') return invoke<LibraryState>('get_library_state')
} }
return { return {
verses: await loadBrowserVerses(), verses: await loadBrowserVersesForTranslation(translationId),
tags: allBrowserTags(), tags: allBrowserTags(),
} }
} }
export async function searchVerses(query: string): 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 })
} }
const trimmed = query.trim().toLowerCase() const trimmed = query.trim().toLowerCase()
const verses = await loadBrowserVerses() const verses = await loadBrowserVersesForTranslation(translationId)
if (!trimmed) return verses if (!trimmed) return verses
if (trimmed.startsWith('tag:')) { if (trimmed.startsWith('tag:')) {
@@ -292,7 +346,7 @@ export async function searchVerses(query: string): Promise<Verse[]> {
} }
if (/^[gh]\d+$/.test(trimmed)) { if (/^[gh]\d+$/.test(trimmed)) {
const strongData = await loadBrowserStrongData() const strongData = await loadBrowserStrongData(translationId)
const matchingVerseIds = strongData.verseIdsByStrong.get(trimmed.toUpperCase()) ?? new Set() const matchingVerseIds = strongData.verseIdsByStrong.get(trimmed.toUpperCase()) ?? new Set()
return verses.filter((verse) => return verses.filter((verse) =>
matchingVerseIds.has(browserVerseKey(verse)), matchingVerseIds.has(browserVerseKey(verse)),
@@ -306,7 +360,12 @@ export async function searchVerses(query: string): Promise<Verse[]> {
) )
} }
export async function addTagToVerse(verseId: number, label: string, color: string) { export async function addTagToVerse(
verseId: number,
label: string,
color: string,
translationId = defaultTranslationId,
) {
if (inTauri()) { if (inTauri()) {
return invoke<Verse>('add_tag_to_verse', { verseId, label, color }) return invoke<Verse>('add_tag_to_verse', { verseId, label, color })
} }
@@ -318,7 +377,7 @@ export async function addTagToVerse(verseId: number, label: string, color: strin
tagsByVerse[verseId] = existing ? current : [...current, tag] tagsByVerse[verseId] = existing ? current : [...current, tag]
writeBrowserTags(tagsByVerse) writeBrowserTags(tagsByVerse)
const verse = (await loadBrowserVerses()).find((item) => item.id === verseId) const verse = (await loadBrowserVersesForTranslation(translationId)).find((item) => item.id === verseId)
if (!verse) throw new Error(`Verse ${verseId} was not found.`) if (!verse) throw new Error(`Verse ${verseId} was not found.`)
return verse return verse
} }
@@ -352,9 +411,14 @@ export async function getDictionaryEntriesForTerms(terms: string[]): Promise<Dic
return entries return entries
} }
async function loadLibreBibleDevVerses(): Promise<Verse[]> { async function loadBrowserCatalog(): Promise<ImportedCatalog> {
browserCatalogCache ??= fetchJson<ImportedCatalog>(`${libreBibleDevRoot}/catalog.json`)
return browserCatalogCache
}
async function loadLibreBibleDevVerses(translationId = defaultTranslationId): Promise<Verse[]> {
const importedVerses = await fetchJsonl<ImportedVerse>( const importedVerses = await fetchJsonl<ImportedVerse>(
`${libreBibleDevRoot}/kjv-eng-kjv2006/verses.jsonl`, `${libreBibleDevRoot}/${translationId}/verses.jsonl`,
) )
browserVerseSourceIds.clear() browserVerseSourceIds.clear()
@@ -369,6 +433,7 @@ async function loadLibreBibleDevVerses(): Promise<Verse[]> {
text: verse.text, text: verse.text,
strongs: [], strongs: [],
footnotes: [], footnotes: [],
studyNotes: [],
tags: [], tags: [],
} }
}) })
@@ -442,16 +507,39 @@ async function loadLibreBibleDevDictionary(): Promise<Map<string, DictionaryEntr
) )
} }
async function loadBrowserFootnotes(): Promise<Map<string, Footnote[]>> { async function loadBrowserFootnotes(translationId = defaultTranslationId): Promise<Map<string, Footnote[]>> {
browserFootnotesCache ??= loadLibreBibleDevFootnotes() if (!browserFootnotesCache.has(translationId)) {
browserFootnotesCache.set(translationId, loadLibreBibleDevFootnotes(translationId))
return browserFootnotesCache
} }
async function loadLibreBibleDevFootnotes(): Promise<Map<string, Footnote[]>> { return browserFootnotesCache.get(translationId)!
}
async function loadBrowserStudyNotes(translationId = defaultTranslationId): Promise<Map<string, Footnote[]>> {
if (!browserStudyNotesCache.has(translationId)) {
browserStudyNotesCache.set(translationId, loadLibreBibleDevStudyNotes(translationId))
}
return browserStudyNotesCache.get(translationId)!
}
async function loadLibreBibleDevFootnotes(translationId = defaultTranslationId): Promise<Map<string, Footnote[]>> {
const importedFootnotes = await fetchJsonl<ImportedFootnote>( const importedFootnotes = await fetchJsonl<ImportedFootnote>(
`${libreBibleDevRoot}/kjv-eng-kjv2006/footnotes.jsonl`, `${libreBibleDevRoot}/${translationId}/footnotes.jsonl`,
) )
return mapImportedNotes(importedFootnotes)
}
async function loadLibreBibleDevStudyNotes(translationId = defaultTranslationId): Promise<Map<string, Footnote[]>> {
const importedFootnotes = await fetchJsonl<ImportedFootnote>(
`${libreBibleDevRoot}/${translationId}/study-notes.jsonl`,
)
return mapImportedNotes(importedFootnotes)
}
function mapImportedNotes(importedFootnotes: ImportedFootnote[]): Map<string, Footnote[]> {
const footnotes = new Map<string, Footnote[]>() const footnotes = new Map<string, Footnote[]>()
for (const note of importedFootnotes) { for (const note of importedFootnotes) {
@@ -463,6 +551,8 @@ async function loadLibreBibleDevFootnotes(): Promise<Map<string, Footnote[]>> {
caller: note.caller, caller: note.caller,
reference: note.reference, reference: note.reference,
text: note.text, text: note.text,
label: note.label,
noteType: note.note_type,
}) })
footnotes.set(note.verse_id, current) footnotes.set(note.verse_id, current)
} }
@@ -470,15 +560,17 @@ async function loadLibreBibleDevFootnotes(): Promise<Map<string, Footnote[]>> {
return footnotes return footnotes
} }
async function loadBrowserStrongData(): Promise<BrowserStrongData> { async function loadBrowserStrongData(translationId = defaultTranslationId): Promise<BrowserStrongData> {
browserStrongDataCache ??= loadLibreBibleDevStrongData() if (!browserStrongDataCache.has(translationId)) {
browserStrongDataCache.set(translationId, loadLibreBibleDevStrongData(translationId))
return browserStrongDataCache
} }
async function loadLibreBibleDevStrongData(): Promise<BrowserStrongData> { return browserStrongDataCache.get(translationId)!
}
async function loadLibreBibleDevStrongData(translationId = defaultTranslationId): Promise<BrowserStrongData> {
const [importedLinks, importedEntries] = await Promise.all([ const [importedLinks, importedEntries] = await Promise.all([
fetchJsonl<ImportedStrongLink>(`${libreBibleDevRoot}/kjv-eng-kjv2006/strongs-links.jsonl`), fetchJsonl<ImportedStrongLink>(`${libreBibleDevRoot}/${translationId}/strongs-links.jsonl`),
fetchJsonl<ImportedStrongEntry>(`${libreBibleDevRoot}/strongs-open-scriptures/entries.jsonl`), fetchJsonl<ImportedStrongEntry>(`${libreBibleDevRoot}/strongs-open-scriptures/entries.jsonl`),
]) ])
@@ -551,3 +643,12 @@ async function fetchJsonl<T>(url: string): Promise<T[]> {
.filter((line) => line.trim().length > 0) .filter((line) => line.trim().length > 0)
.map((line) => JSON.parse(line) as T) .map((line) => JSON.parse(line) as T)
} }
async function fetchJson<T>(url: string): Promise<T> {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Unable to load ${url}: ${response.status} ${response.statusText}`)
}
return response.json() as Promise<T>
}
+15
View File
@@ -6,6 +6,7 @@ export type Verse = {
text: string text: string
strongs: StrongLink[] strongs: StrongLink[]
footnotes?: Footnote[] footnotes?: Footnote[]
studyNotes?: StudyNote[]
commentary?: CommentaryEntry[] commentary?: CommentaryEntry[]
dictionary?: DictionaryEntry[] dictionary?: DictionaryEntry[]
tags: Tag[] tags: Tag[]
@@ -34,8 +35,12 @@ export type Footnote = {
caller: string caller: string
reference: string reference: string
text: string text: string
label?: string
noteType?: string
} }
export type StudyNote = Footnote
export type CommentaryEntry = { export type CommentaryEntry = {
id: string id: string
resourceId: string resourceId: string
@@ -72,3 +77,13 @@ export type LibraryState = {
verses: Verse[] verses: Verse[]
tags: Tag[] tags: Tag[]
} }
export type TranslationResource = {
id: string
title: string
shortTitle: string
abbreviation: string
licenseName: string
attribution: string
restrictions: string
}