Load translations from LibreBible catalog
This commit is contained in:
+154
-26
@@ -4,10 +4,11 @@ import {
|
||||
getCommentaryForVerse,
|
||||
getDictionaryEntriesForTerms,
|
||||
getLibraryState,
|
||||
getTranslations,
|
||||
searchVerses,
|
||||
} from './studyClient'
|
||||
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'
|
||||
|
||||
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 studySettingsStorageKey = 'libre-study.study-settings.v1'
|
||||
const timelineLimit = 200
|
||||
const defaultTranslationId = 'kjv-eng-kjv2006'
|
||||
type ViewMode = 'page' | 'study' | 'notes' | 'compare'
|
||||
type StrongDisplayMode = 'off' | 'highlight' | 'numbers'
|
||||
type PageMotion = 'idle' | 'turn-forward' | 'turn-back'
|
||||
@@ -54,7 +56,7 @@ function reference(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 id =
|
||||
typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
||||
@@ -64,7 +66,7 @@ function createSession(title = 'Study Session'): StudySession {
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
baseTranslation: 'kjv-eng-kjv2006',
|
||||
baseTranslation,
|
||||
footnotesEnabled: true,
|
||||
strongsEnabled: true,
|
||||
commentaryEnabled: true,
|
||||
@@ -227,6 +229,7 @@ function App() {
|
||||
const [themeMode, setThemeMode] = useState<ThemeMode>('light')
|
||||
const [libraryVerses, setLibraryVerses] = useState<Verse[]>([])
|
||||
const [verses, setVerses] = useState<Verse[]>([])
|
||||
const [translations, setTranslations] = useState<TranslationResource[]>([])
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null)
|
||||
const [pageStartId, setPageStartId] = useState<number | null>(null)
|
||||
const [selectedStrong, setSelectedStrong] = useState<string | null>(null)
|
||||
@@ -236,6 +239,7 @@ function App() {
|
||||
const [sessions, setSessions] = useState<StudySession[]>([])
|
||||
const [activeSessionId, setActiveSessionId] = useState('')
|
||||
const [newSessionTitle, setNewSessionTitle] = useState('New KJV Study')
|
||||
const [newSessionBaseTranslation, setNewSessionBaseTranslation] = useState(defaultTranslationId)
|
||||
const [newSessionFootnotes, setNewSessionFootnotes] = useState(true)
|
||||
const [newSessionStrongs, setNewSessionStrongs] = useState(true)
|
||||
const [newSessionCommentary, setNewSessionCommentary] = useState(true)
|
||||
@@ -246,6 +250,7 @@ function App() {
|
||||
const [originalCollapsed, setOriginalCollapsed] = useState(false)
|
||||
const [linkedCollapsed, setLinkedCollapsed] = useState(false)
|
||||
const [definitionCollapsed, setDefinitionCollapsed] = useState(false)
|
||||
const [studyNotesCollapsed, setStudyNotesCollapsed] = useState(false)
|
||||
const [commentaryCollapsed, setCommentaryCollapsed] = useState(false)
|
||||
const [dictionaryCollapsed, setDictionaryCollapsed] = useState(false)
|
||||
const [showOriginalLine, setShowOriginalLine] = useState(true)
|
||||
@@ -264,9 +269,9 @@ function App() {
|
||||
const notesDragOpenTimeout = useRef<number | null>(null)
|
||||
const readerRef = useRef<HTMLElement | null>(null)
|
||||
|
||||
async function loadLibrary() {
|
||||
async function loadLibrary(translationId = activeSession?.baseTranslation ?? defaultTranslationId) {
|
||||
setError(null)
|
||||
const state = await getLibraryState()
|
||||
const state = await getLibraryState(translationId)
|
||||
setLibraryVerses(state.verses)
|
||||
setVerses(state.verses)
|
||||
const firstId = state.verses[0]?.id ?? null
|
||||
@@ -352,7 +357,7 @@ function App() {
|
||||
setError(null)
|
||||
const trimmed = nextQuery.trim()
|
||||
setSelectedStrong(/^[gh]\d+$/i.test(trimmed) ? trimmed.toUpperCase() : null)
|
||||
const results = await searchVerses(nextQuery)
|
||||
const results = await searchVerses(nextQuery, activeSession?.baseTranslation ?? defaultTranslationId)
|
||||
setVerses(results)
|
||||
const firstResult = results[0]
|
||||
setSelectedId(firstResult?.id ?? null)
|
||||
@@ -375,7 +380,12 @@ function App() {
|
||||
if (!selectedId || !label) return
|
||||
|
||||
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) =>
|
||||
current.map((verse) => (verse.id === updated.id ? updated : verse)),
|
||||
@@ -400,6 +410,7 @@ function App() {
|
||||
originalCollapsed: boolean
|
||||
linkedCollapsed: boolean
|
||||
definitionCollapsed: boolean
|
||||
studyNotesCollapsed: boolean
|
||||
commentaryCollapsed: boolean
|
||||
dictionaryCollapsed: boolean
|
||||
showOriginalLine: boolean
|
||||
@@ -416,6 +427,7 @@ function App() {
|
||||
setOriginalCollapsed(parsed.originalCollapsed ?? false)
|
||||
setLinkedCollapsed(parsed.linkedCollapsed ?? false)
|
||||
setDefinitionCollapsed(parsed.definitionCollapsed ?? false)
|
||||
setStudyNotesCollapsed(parsed.studyNotesCollapsed ?? false)
|
||||
setCommentaryCollapsed(parsed.commentaryCollapsed ?? false)
|
||||
setDictionaryCollapsed(parsed.dictionaryCollapsed ?? false)
|
||||
setShowOriginalLine(parsed.showOriginalLine ?? true)
|
||||
@@ -447,7 +459,7 @@ function App() {
|
||||
nextSessions = nextSessions.map((session) => ({
|
||||
...createSession(session.title),
|
||||
...session,
|
||||
baseTranslation: session.baseTranslation ?? 'kjv-eng-kjv2006',
|
||||
baseTranslation: session.baseTranslation ?? defaultTranslationId,
|
||||
footnotesEnabled: session.footnotesEnabled ?? true,
|
||||
strongsEnabled: session.strongsEnabled ?? true,
|
||||
commentaryEnabled: session.commentaryEnabled ?? true,
|
||||
@@ -455,14 +467,17 @@ function App() {
|
||||
timelineEnabled: session.timelineEnabled ?? true,
|
||||
}))
|
||||
|
||||
setSessions(nextSessions)
|
||||
setActiveSessionId(
|
||||
nextSessions.some((session) => session.id === savedActiveSessionId)
|
||||
? savedActiveSessionId ?? nextSessions[0].id
|
||||
: nextSessions[0].id,
|
||||
)
|
||||
const selectedSessionId = nextSessions.some((session) => session.id === savedActiveSessionId)
|
||||
? savedActiveSessionId ?? 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)))
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
@@ -488,6 +503,7 @@ function App() {
|
||||
originalCollapsed,
|
||||
linkedCollapsed,
|
||||
definitionCollapsed,
|
||||
studyNotesCollapsed,
|
||||
commentaryCollapsed,
|
||||
dictionaryCollapsed,
|
||||
showOriginalLine,
|
||||
@@ -515,6 +531,7 @@ function App() {
|
||||
showLinkedLine,
|
||||
showOriginalLine,
|
||||
splitPercent,
|
||||
studyNotesCollapsed,
|
||||
themeMode,
|
||||
])
|
||||
|
||||
@@ -527,6 +544,7 @@ function App() {
|
||||
setViewMode(activeSession.viewMode)
|
||||
setStrongDisplay(activeSession.strongDisplay)
|
||||
setNoteDraft(activeSession.noteDraft)
|
||||
loadLibrary(activeSession.baseTranslation).catch((reason) => setError(String(reason)))
|
||||
setSelectedId((current) => activeSession.selectedId ?? current)
|
||||
setPageStartId((current) => activeSession.pageStartId ?? current)
|
||||
setSelectedStrong(null)
|
||||
@@ -550,6 +568,12 @@ function App() {
|
||||
() => sessions.find((session) => session.id === activeSessionId) ?? sessions[0],
|
||||
[activeSessionId, sessions],
|
||||
)
|
||||
const activeTranslation = useMemo(
|
||||
() =>
|
||||
translations.find((translation) => translation.id === activeSession?.baseTranslation) ??
|
||||
translations.find((translation) => translation.id === defaultTranslationId),
|
||||
[activeSession?.baseTranslation, translations],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
@@ -623,6 +647,7 @@ function App() {
|
||||
? `${reference(pageVerses[0])} - ${reference(pageVerses[pageVerses.length - 1])}`
|
||||
: 'No page loaded'
|
||||
const selectedFootnotes = selectedVerse?.footnotes ?? []
|
||||
const selectedStudyNotes = selectedVerse?.studyNotes ?? []
|
||||
const pageFootnotes = useMemo(
|
||||
() => pageVerses.flatMap((verse) => verse.footnotes ?? []),
|
||||
[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) {
|
||||
if (!selectedVerse) return
|
||||
const currentStartIndex = pageAnchors.findIndex((verse) => verse.id === pageStartId)
|
||||
@@ -851,7 +908,10 @@ function App() {
|
||||
|
||||
function startNewSession() {
|
||||
const nextSession = {
|
||||
...createSession(newSessionTitle.trim() || `Study Session ${sessions.length + 1}`),
|
||||
...createSession(
|
||||
newSessionTitle.trim() || `Study Session ${sessions.length + 1}`,
|
||||
newSessionBaseTranslation,
|
||||
),
|
||||
footnotesEnabled: newSessionFootnotes,
|
||||
strongsEnabled: newSessionStrongs,
|
||||
commentaryEnabled: newSessionCommentary,
|
||||
@@ -860,6 +920,7 @@ function App() {
|
||||
}
|
||||
setSessions((current) => [nextSession, ...current])
|
||||
setActiveSessionId(nextSession.id)
|
||||
loadLibrary(nextSession.baseTranslation).catch((reason) => setError(String(reason)))
|
||||
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() {
|
||||
if (!showCommentaryPanel) return null
|
||||
|
||||
@@ -1248,7 +1355,7 @@ function App() {
|
||||
)
|
||||
}
|
||||
|
||||
function renderBiblePage(label = 'KJV') {
|
||||
function renderBiblePage(label = activeTranslation?.abbreviation ?? 'Bible') {
|
||||
if (!selectedVerse) return null
|
||||
return (
|
||||
<article className="bible-page" aria-label={`${label} Bible page`}>
|
||||
@@ -1368,6 +1475,7 @@ function App() {
|
||||
</div>
|
||||
)}
|
||||
<div className="margin-resource-grid">
|
||||
{renderStudyNotesSection()}
|
||||
{renderCommentarySection()}
|
||||
{renderDictionarySection()}
|
||||
</div>
|
||||
@@ -1543,7 +1651,7 @@ function App() {
|
||||
<dl className="session-facts">
|
||||
<div>
|
||||
<dt>Translation</dt>
|
||||
<dd>KJV</dd>
|
||||
<dd>{activeTranslation?.abbreviation ?? activeSession?.baseTranslation ?? 'n/a'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Current page</dt>
|
||||
@@ -1613,8 +1721,15 @@ function App() {
|
||||
</label>
|
||||
<label>
|
||||
Base translation
|
||||
<select value="kjv-eng-kjv2006" onChange={() => null}>
|
||||
<option value="kjv-eng-kjv2006">King James Version</option>
|
||||
<select
|
||||
value={newSessionBaseTranslation}
|
||||
onChange={(event) => setNewSessionBaseTranslation(event.target.value)}
|
||||
>
|
||||
{translations.map((translation) => (
|
||||
<option key={translation.id} value={translation.id}>
|
||||
{translation.shortTitle}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<div className="resource-toggles">
|
||||
@@ -1702,7 +1817,11 @@ function App() {
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<dt>Software stack</dt>
|
||||
@@ -2053,8 +2172,17 @@ function App() {
|
||||
<div className="page-controls" aria-label="Page selectors">
|
||||
<label>
|
||||
Translation
|
||||
<select value="kjv-eng-kjv2006" onChange={() => null}>
|
||||
<option value="kjv-eng-kjv2006">KJV</option>
|
||||
<select
|
||||
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>
|
||||
</label>
|
||||
<label>
|
||||
@@ -2129,7 +2257,7 @@ function App() {
|
||||
id="search"
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder='Search KJV, reference, or Strong's'
|
||||
placeholder={`Search ${activeTranslation?.abbreviation ?? 'Bible'}, reference, or Strong's`}
|
||||
/>
|
||||
<button type="submit">Go</button>
|
||||
</form>
|
||||
@@ -2149,7 +2277,7 @@ function App() {
|
||||
{viewMode === 'compare' ? (
|
||||
<div className="page-system compare-system" aria-label="Compare translations">
|
||||
<div className="compare-pages">
|
||||
{renderBiblePage('KJV')}
|
||||
{renderBiblePage()}
|
||||
<article className="bible-page compare-placeholder" aria-label="Second translation placeholder">
|
||||
<header>
|
||||
<span>Second Translation</span>
|
||||
@@ -2168,7 +2296,7 @@ function App() {
|
||||
) : (
|
||||
<>
|
||||
<div className={`page-system ${viewMode} ${pageMotion}`}>
|
||||
{renderBiblePage('KJV')}
|
||||
{renderBiblePage()}
|
||||
{renderFootnoteStrip()}
|
||||
{renderPageTurner()}
|
||||
{renderBookTabs()}
|
||||
|
||||
Reference in New Issue
Block a user