From b91912649cc513369115df6566a7b23cf8613344 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 12 Jul 2026 11:11:54 -0500 Subject: [PATCH] Load translations from LibreBible catalog --- 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 | 180 ++++++++++++++++++++++++++++++++------ src/studyClient.ts | 159 +++++++++++++++++++++++++++------ src/types.ts | 15 ++++ 8 files changed, 305 insertions(+), 61 deletions(-) diff --git a/package-lock.json b/package-lock.json index d0acdea..308aa6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "libre-study", - "version": "0.1.18", + "version": "0.1.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "libre-study", - "version": "0.1.18", + "version": "0.1.19", "dependencies": { "@tauri-apps/api": "^2.11.1", "react": "^19.2.7", diff --git a/package.json b/package.json index e9c592d..3cf4895 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "libre-study", "private": true, - "version": "0.1.18", + "version": "0.1.19", "type": "module", "scripts": { "dev": "vite --host 127.0.0.1", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 372564e..42fa6f3 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -77,7 +77,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "app" -version = "0.1.18" +version = "0.1.19" dependencies = [ "log", "rusqlite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 88d29bc..3f42959 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "app" -version = "0.1.18" +version = "0.1.19" description = "A Tauri App" authors = ["you"] license = "" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 1113aa8..d7609a1 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.18", + "version": "0.1.19", "identifier": "org.librestudy.desktop", "build": { "frontendDist": "../dist", diff --git a/src/App.tsx b/src/App.tsx index da8eecf..8cb8ea8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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('light') const [libraryVerses, setLibraryVerses] = useState([]) const [verses, setVerses] = useState([]) + const [translations, setTranslations] = useState([]) const [selectedId, setSelectedId] = useState(null) const [pageStartId, setPageStartId] = useState(null) const [selectedStrong, setSelectedStrong] = useState(null) @@ -236,6 +239,7 @@ function App() { const [sessions, setSessions] = useState([]) 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(null) const readerRef = useRef(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 ( +
+
{ + 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') + } + }} + > + + {selectedStudyNotes.length ? `${selectedStudyNotes.length} notes` : 'None'} +
+ {!studyNotesCollapsed ? ( + selectedStudyNotes.length ? ( +
+ {selectedStudyNotes.map((note) => ( +

startTextDrag(event, `${note.reference} ${note.text}`)} + > + {note.reference} + {note.text} +

+ ))} +
+ ) : ( +

No translation study notes for the selected verse.

+ ) + ) : null} +
+ ) + } + 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 (
@@ -1368,6 +1475,7 @@ function App() { )}
+ {renderStudyNotesSection()} {renderCommentarySection()} {renderDictionarySection()}
@@ -1543,7 +1651,7 @@ function App() {
Translation
-
KJV
+
{activeTranslation?.abbreviation ?? activeSession?.baseTranslation ?? 'n/a'}
Current page
@@ -1613,8 +1721,15 @@ function App() {
@@ -1702,7 +1817,11 @@ function App() {
Current Bible package
-
King James Version, public domain KJV text package with Strong's-linked study data.
+
+ {activeTranslation + ? `${activeTranslation.title}. ${activeTranslation.licenseName}` + : 'LibreBible translation package loaded from the local catalog.'} +
Software stack
@@ -2053,8 +2172,17 @@ function App() {