Wire dynamic commentary and dictionary margin resources
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "libre-study",
|
"name": "libre-study",
|
||||||
"version": "0.1.14",
|
"version": "0.1.15",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "libre-study",
|
"name": "libre-study",
|
||||||
"version": "0.1.14",
|
"version": "0.1.15",
|
||||||
"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.14",
|
"version": "0.1.15",
|
||||||
"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.14"
|
version = "0.1.15"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "0.1.14"
|
version = "0.1.15"
|
||||||
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.14",
|
"version": "0.1.15",
|
||||||
"identifier": "org.librestudy.desktop",
|
"identifier": "org.librestudy.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
|
|||||||
+52
@@ -635,6 +635,58 @@ textarea:focus-visible {
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.resource-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 9px;
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #dfd6c2;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fffdf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commentary-list,
|
||||||
|
.dictionary-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commentary-list section,
|
||||||
|
.dictionary-list section {
|
||||||
|
display: grid;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commentary-list h3,
|
||||||
|
.dictionary-list h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #252821;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commentary-list p,
|
||||||
|
.dictionary-list p {
|
||||||
|
margin: 0;
|
||||||
|
color: #3f443d;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.48;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commentary-list small {
|
||||||
|
color: #657061;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dictionary-list p strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
color: #657061;
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
.notes-workspace {
|
.notes-workspace {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
+263
-4
@@ -1,7 +1,13 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { addTagToVerse, getLibraryState, searchVerses } from './studyClient'
|
import {
|
||||||
|
addTagToVerse,
|
||||||
|
getCommentaryForVerse,
|
||||||
|
getDictionaryEntriesForTerms,
|
||||||
|
getLibraryState,
|
||||||
|
searchVerses,
|
||||||
|
} from './studyClient'
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import type { Verse } from './types'
|
import type { CommentaryEntry, DictionaryEntry, Verse } from './types'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
|
||||||
const tagColors = ['#2f6f5e', '#735c0f', '#8a3ffc', '#b42318', '#245ba7']
|
const tagColors = ['#2f6f5e', '#735c0f', '#8a3ffc', '#b42318', '#245ba7']
|
||||||
@@ -30,6 +36,8 @@ type StudySession = {
|
|||||||
baseTranslation: string
|
baseTranslation: string
|
||||||
footnotesEnabled: boolean
|
footnotesEnabled: boolean
|
||||||
strongsEnabled: boolean
|
strongsEnabled: boolean
|
||||||
|
commentaryEnabled: boolean
|
||||||
|
dictionaryEnabled: boolean
|
||||||
timelineEnabled: boolean
|
timelineEnabled: boolean
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
@@ -58,6 +66,8 @@ function createSession(title = 'Study Session'): StudySession {
|
|||||||
baseTranslation: 'kjv-eng-kjv2006',
|
baseTranslation: 'kjv-eng-kjv2006',
|
||||||
footnotesEnabled: true,
|
footnotesEnabled: true,
|
||||||
strongsEnabled: true,
|
strongsEnabled: true,
|
||||||
|
commentaryEnabled: true,
|
||||||
|
dictionaryEnabled: true,
|
||||||
timelineEnabled: true,
|
timelineEnabled: true,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
@@ -151,6 +161,57 @@ function abbreviateBook(book: string) {
|
|||||||
return abbreviations[book] ?? book.slice(0, 4)
|
return abbreviations[book] ?? book.slice(0, 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dictionaryTermsForVerse(verse: Verse) {
|
||||||
|
const stopWords = new Set([
|
||||||
|
'a',
|
||||||
|
'an',
|
||||||
|
'and',
|
||||||
|
'are',
|
||||||
|
'as',
|
||||||
|
'at',
|
||||||
|
'be',
|
||||||
|
'but',
|
||||||
|
'by',
|
||||||
|
'for',
|
||||||
|
'from',
|
||||||
|
'he',
|
||||||
|
'her',
|
||||||
|
'him',
|
||||||
|
'his',
|
||||||
|
'i',
|
||||||
|
'in',
|
||||||
|
'is',
|
||||||
|
'it',
|
||||||
|
'of',
|
||||||
|
'on',
|
||||||
|
'or',
|
||||||
|
'that',
|
||||||
|
'the',
|
||||||
|
'their',
|
||||||
|
'them',
|
||||||
|
'they',
|
||||||
|
'this',
|
||||||
|
'to',
|
||||||
|
'unto',
|
||||||
|
'was',
|
||||||
|
'were',
|
||||||
|
'with',
|
||||||
|
'ye',
|
||||||
|
'you',
|
||||||
|
])
|
||||||
|
const strongSurfaces = verse.strongs.map((entry) => entry.surface)
|
||||||
|
const textWords = [...verse.text.matchAll(/[A-Za-z][A-Za-z'-]*/g)].map((match) => match[0])
|
||||||
|
const unique = new Map<string, string>()
|
||||||
|
|
||||||
|
for (const term of [...strongSurfaces, ...textWords]) {
|
||||||
|
const normalized = term.toLowerCase().replace(/[^a-z0-9\s'-]/g, '').trim()
|
||||||
|
if (!normalized || stopWords.has(normalized) || normalized.length < 3) continue
|
||||||
|
unique.set(normalized, term)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...unique.values()].slice(0, 28)
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [railOpen, setRailOpen] = useState(false)
|
const [railOpen, setRailOpen] = useState(false)
|
||||||
const [sessionModalOpen, setSessionModalOpen] = useState(false)
|
const [sessionModalOpen, setSessionModalOpen] = useState(false)
|
||||||
@@ -173,12 +234,19 @@ function App() {
|
|||||||
const [newSessionTitle, setNewSessionTitle] = useState('New KJV Study')
|
const [newSessionTitle, setNewSessionTitle] = useState('New KJV Study')
|
||||||
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 [newSessionDictionary, setNewSessionDictionary] = useState(true)
|
||||||
const [newSessionTimeline, setNewSessionTimeline] = useState(true)
|
const [newSessionTimeline, setNewSessionTimeline] = useState(true)
|
||||||
const [lemmaFontSize, setLemmaFontSize] = useState(17)
|
const [lemmaFontSize, setLemmaFontSize] = useState(17)
|
||||||
const [linkedFontSize, setLinkedFontSize] = useState(15)
|
const [linkedFontSize, setLinkedFontSize] = useState(15)
|
||||||
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 [commentaryCollapsed, setCommentaryCollapsed] = useState(false)
|
||||||
|
const [dictionaryCollapsed, setDictionaryCollapsed] = useState(false)
|
||||||
|
const [commentaryEntries, setCommentaryEntries] = useState<CommentaryEntry[]>([])
|
||||||
|
const [dictionaryEntries, setDictionaryEntries] = useState<DictionaryEntry[]>([])
|
||||||
|
const [marginResourcesLoading, setMarginResourcesLoading] = useState(false)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const restoredSessionId = useRef('')
|
const restoredSessionId = useRef('')
|
||||||
@@ -350,6 +418,8 @@ function App() {
|
|||||||
baseTranslation: session.baseTranslation ?? 'kjv-eng-kjv2006',
|
baseTranslation: session.baseTranslation ?? 'kjv-eng-kjv2006',
|
||||||
footnotesEnabled: session.footnotesEnabled ?? true,
|
footnotesEnabled: session.footnotesEnabled ?? true,
|
||||||
strongsEnabled: session.strongsEnabled ?? true,
|
strongsEnabled: session.strongsEnabled ?? true,
|
||||||
|
commentaryEnabled: session.commentaryEnabled ?? true,
|
||||||
|
dictionaryEnabled: session.dictionaryEnabled ?? true,
|
||||||
timelineEnabled: session.timelineEnabled ?? true,
|
timelineEnabled: session.timelineEnabled ?? true,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -422,6 +492,44 @@ function App() {
|
|||||||
() => sessions.find((session) => session.id === activeSessionId) ?? sessions[0],
|
() => sessions.find((session) => session.id === activeSessionId) ?? sessions[0],
|
||||||
[activeSessionId, sessions],
|
[activeSessionId, sessions],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
|
async function loadMarginResources() {
|
||||||
|
if (!selectedVerse) {
|
||||||
|
setCommentaryEntries([])
|
||||||
|
setDictionaryEntries([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setMarginResourcesLoading(true)
|
||||||
|
try {
|
||||||
|
const [nextCommentary, nextDictionary] = await Promise.all([
|
||||||
|
activeSession?.commentaryEnabled === false
|
||||||
|
? Promise.resolve([])
|
||||||
|
: getCommentaryForVerse(selectedVerse),
|
||||||
|
activeSession?.dictionaryEnabled === false
|
||||||
|
? Promise.resolve([])
|
||||||
|
: getDictionaryEntriesForTerms(dictionaryTermsForVerse(selectedVerse)),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (cancelled) return
|
||||||
|
setCommentaryEntries(nextCommentary)
|
||||||
|
setDictionaryEntries(nextDictionary)
|
||||||
|
} catch (reason) {
|
||||||
|
if (!cancelled) setError(String(reason))
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) setMarginResourcesLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMarginResources()
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [activeSession?.commentaryEnabled, activeSession?.dictionaryEnabled, selectedVerse])
|
||||||
|
|
||||||
const pageAnchors = useMemo(() => {
|
const pageAnchors = useMemo(() => {
|
||||||
const source = libraryVerses.length ? libraryVerses : verses
|
const source = libraryVerses.length ? libraryVerses : verses
|
||||||
const anchors: Verse[] = []
|
const anchors: Verse[] = []
|
||||||
@@ -629,6 +737,8 @@ function App() {
|
|||||||
...createSession(newSessionTitle.trim() || `Study Session ${sessions.length + 1}`),
|
...createSession(newSessionTitle.trim() || `Study Session ${sessions.length + 1}`),
|
||||||
footnotesEnabled: newSessionFootnotes,
|
footnotesEnabled: newSessionFootnotes,
|
||||||
strongsEnabled: newSessionStrongs,
|
strongsEnabled: newSessionStrongs,
|
||||||
|
commentaryEnabled: newSessionCommentary,
|
||||||
|
dictionaryEnabled: newSessionDictionary,
|
||||||
timelineEnabled: newSessionTimeline,
|
timelineEnabled: newSessionTimeline,
|
||||||
}
|
}
|
||||||
setSessions((current) => [nextSession, ...current])
|
setSessions((current) => [nextSession, ...current])
|
||||||
@@ -641,7 +751,12 @@ function App() {
|
|||||||
if (patch.strongsEnabled === false) {
|
if (patch.strongsEnabled === false) {
|
||||||
setStrongDisplay('off')
|
setStrongDisplay('off')
|
||||||
}
|
}
|
||||||
if (typeof patch.strongsEnabled === 'boolean' || typeof patch.footnotesEnabled === 'boolean') {
|
if (
|
||||||
|
typeof patch.strongsEnabled === 'boolean' ||
|
||||||
|
typeof patch.footnotesEnabled === 'boolean' ||
|
||||||
|
typeof patch.commentaryEnabled === 'boolean' ||
|
||||||
|
typeof patch.dictionaryEnabled === 'boolean'
|
||||||
|
) {
|
||||||
recordTimeline({ type: 'setting', label: 'Updated session resources' }, patch)
|
recordTimeline({ type: 'setting', label: 'Updated session resources' }, patch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -873,6 +988,112 @@ function App() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderCommentarySection() {
|
||||||
|
if (activeSession?.commentaryEnabled === false) {
|
||||||
|
return (
|
||||||
|
<article className="resource-card">
|
||||||
|
<header className="margin-section-header">
|
||||||
|
<button type="button" className="collapse-button">Commentary</button>
|
||||||
|
<span>Off</span>
|
||||||
|
</header>
|
||||||
|
<p className="resource-disabled">Commentary is disabled for this session.</p>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className="resource-card commentary-card" aria-label="Commentary">
|
||||||
|
<header
|
||||||
|
className="margin-section-header"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => setCommentaryCollapsed((current) => !current)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault()
|
||||||
|
setCommentaryCollapsed((current) => !current)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button type="button" className="collapse-button">Commentary</button>
|
||||||
|
<span>{commentaryEntries.length ? 'MHCC' : marginResourcesLoading ? 'Loading' : 'None'}</span>
|
||||||
|
</header>
|
||||||
|
{!commentaryCollapsed ? (
|
||||||
|
commentaryEntries.length ? (
|
||||||
|
<div className="commentary-list">
|
||||||
|
{commentaryEntries.map((entry) => (
|
||||||
|
<section key={entry.id}>
|
||||||
|
<h3>{entry.reference}</h3>
|
||||||
|
<p>{entry.text}</p>
|
||||||
|
<small>{entry.source}</small>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="resource-disabled">
|
||||||
|
{marginResourcesLoading ? 'Loading commentary...' : 'No commentary matched this verse.'}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDictionarySection() {
|
||||||
|
if (activeSession?.dictionaryEnabled === false) {
|
||||||
|
return (
|
||||||
|
<article className="resource-card">
|
||||||
|
<header className="margin-section-header">
|
||||||
|
<button type="button" className="collapse-button">Dictionary</button>
|
||||||
|
<span>Off</span>
|
||||||
|
</header>
|
||||||
|
<p className="resource-disabled">Dictionary lookup is disabled for this session.</p>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className="resource-card dictionary-card" aria-label="Dictionary">
|
||||||
|
<header
|
||||||
|
className="margin-section-header"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => setDictionaryCollapsed((current) => !current)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault()
|
||||||
|
setDictionaryCollapsed((current) => !current)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button type="button" className="collapse-button">Dictionary</button>
|
||||||
|
<span>{dictionaryEntries.length ? `${dictionaryEntries.length} terms` : marginResourcesLoading ? 'Loading' : 'None'}</span>
|
||||||
|
</header>
|
||||||
|
{!dictionaryCollapsed ? (
|
||||||
|
dictionaryEntries.length ? (
|
||||||
|
<div className="dictionary-list">
|
||||||
|
{dictionaryEntries.slice(0, 6).map((entry) => (
|
||||||
|
<section key={entry.id}>
|
||||||
|
<h3>{entry.term}</h3>
|
||||||
|
{entry.definitions.slice(0, 2).map((definition) => (
|
||||||
|
<p key={`${entry.id}-${definition.source}`}>
|
||||||
|
<strong>{definition.sourceName}</strong>
|
||||||
|
{definition.text}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="resource-disabled">
|
||||||
|
{marginResourcesLoading ? 'Loading dictionary...' : 'No dictionary terms matched this verse.'}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function renderBiblePage(label = 'KJV') {
|
function renderBiblePage(label = 'KJV') {
|
||||||
if (!selectedVerse) return null
|
if (!selectedVerse) return null
|
||||||
return (
|
return (
|
||||||
@@ -918,7 +1139,11 @@ function App() {
|
|||||||
<aside className="study-margin" aria-label="Study margin">
|
<aside className="study-margin" aria-label="Study margin">
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
<h2>Study Margin</h2>
|
<h2>Study Margin</h2>
|
||||||
<span>{activeSession?.strongsEnabled === false ? 'disabled' : `${selectedVerse.strongs.length} links`}</span>
|
<span>
|
||||||
|
{activeSession?.strongsEnabled === false
|
||||||
|
? 'links off'
|
||||||
|
: `${selectedVerse.strongs.length} links`}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="margin-reference">{reference(selectedVerse)}</p>
|
<p className="margin-reference">{reference(selectedVerse)}</p>
|
||||||
{activeSession?.strongsEnabled === false ? (
|
{activeSession?.strongsEnabled === false ? (
|
||||||
@@ -967,6 +1192,8 @@ function App() {
|
|||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{renderCommentarySection()}
|
||||||
|
{renderDictionarySection()}
|
||||||
</aside>
|
</aside>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1169,6 +1396,22 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
Footnotes
|
Footnotes
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={activeSession?.commentaryEnabled ?? true}
|
||||||
|
onChange={(event) => setActiveSessionOption({ commentaryEnabled: event.target.checked })}
|
||||||
|
/>
|
||||||
|
Commentary
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={activeSession?.dictionaryEnabled ?? true}
|
||||||
|
onChange={(event) => setActiveSessionOption({ dictionaryEnabled: event.target.checked })}
|
||||||
|
/>
|
||||||
|
Dictionary
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -1212,6 +1455,22 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
Include footnotes
|
Include footnotes
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={newSessionCommentary}
|
||||||
|
onChange={(event) => setNewSessionCommentary(event.target.checked)}
|
||||||
|
/>
|
||||||
|
Include commentary
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={newSessionDictionary}
|
||||||
|
onChange={(event) => setNewSessionDictionary(event.target.checked)}
|
||||||
|
/>
|
||||||
|
Include dictionary
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|||||||
+138
-1
@@ -1,5 +1,5 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import type { Footnote, LibraryState, StrongLink, Tag, Verse } from './types'
|
import type { CommentaryEntry, DictionaryEntry, Footnote, LibraryState, StrongLink, Tag, Verse } from './types'
|
||||||
|
|
||||||
type TauriWindow = Window &
|
type TauriWindow = Window &
|
||||||
typeof globalThis & {
|
typeof globalThis & {
|
||||||
@@ -43,6 +43,36 @@ type ImportedStrongEntry = {
|
|||||||
kjv_definition?: string
|
kjv_definition?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ImportedCommentaryEntry = {
|
||||||
|
id: string
|
||||||
|
resource_id: string
|
||||||
|
source: string
|
||||||
|
book: string
|
||||||
|
chapter: number
|
||||||
|
verse_start: number
|
||||||
|
verse_end: number
|
||||||
|
reference: string
|
||||||
|
title: string
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImportedDictionaryEntry = {
|
||||||
|
id: string
|
||||||
|
resource_id: string
|
||||||
|
term: string
|
||||||
|
slug: string
|
||||||
|
definitions: Array<{
|
||||||
|
source: string
|
||||||
|
source_name: string
|
||||||
|
text: string
|
||||||
|
}>
|
||||||
|
scripture_refs: Array<{
|
||||||
|
reference: string
|
||||||
|
original: string
|
||||||
|
}>
|
||||||
|
sources: string[]
|
||||||
|
}
|
||||||
|
|
||||||
type BrowserLexiconEntry = {
|
type BrowserLexiconEntry = {
|
||||||
lemma: string
|
lemma: string
|
||||||
transliteration: string
|
transliteration: string
|
||||||
@@ -58,6 +88,8 @@ type BrowserStrongData = {
|
|||||||
let browserLibraryCache: Promise<Verse[]> | null = null
|
let browserLibraryCache: Promise<Verse[]> | null = null
|
||||||
let browserStrongDataCache: Promise<BrowserStrongData> | null = null
|
let browserStrongDataCache: Promise<BrowserStrongData> | null = null
|
||||||
let browserFootnotesCache: Promise<Map<string, Footnote[]>> | null = null
|
let browserFootnotesCache: Promise<Map<string, Footnote[]>> | null = null
|
||||||
|
let browserCommentaryCache: Promise<Map<string, CommentaryEntry[]>> | null = null
|
||||||
|
let browserDictionaryCache: Promise<Map<string, DictionaryEntry>> | null = null
|
||||||
const browserVerseSourceIds = new Map<number, string>()
|
const browserVerseSourceIds = new Map<number, string>()
|
||||||
|
|
||||||
const fallbackBrowserVerses: Verse[] = [
|
const fallbackBrowserVerses: Verse[] = [
|
||||||
@@ -291,6 +323,35 @@ export async function addTagToVerse(verseId: number, label: string, color: strin
|
|||||||
return verse
|
return verse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCommentaryForVerse(verse: Verse): Promise<CommentaryEntry[]> {
|
||||||
|
if (inTauri()) return []
|
||||||
|
|
||||||
|
const commentary = await loadBrowserCommentary()
|
||||||
|
const chapterEntries = commentary.get(commentaryKey(verse.book, verse.chapter)) ?? []
|
||||||
|
return chapterEntries.filter(
|
||||||
|
(entry) => verse.verse >= entry.verseStart && verse.verse <= entry.verseEnd,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDictionaryEntriesForTerms(terms: string[]): Promise<DictionaryEntry[]> {
|
||||||
|
if (inTauri()) return []
|
||||||
|
|
||||||
|
const dictionary = await loadBrowserDictionary()
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const entries: DictionaryEntry[] = []
|
||||||
|
|
||||||
|
for (const term of terms) {
|
||||||
|
const normalized = normalizeTerm(term)
|
||||||
|
if (!normalized || seen.has(normalized)) continue
|
||||||
|
const entry = dictionary.get(normalized)
|
||||||
|
if (!entry) continue
|
||||||
|
seen.add(normalized)
|
||||||
|
entries.push(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
async function loadLibreBibleDevVerses(): Promise<Verse[]> {
|
async function loadLibreBibleDevVerses(): Promise<Verse[]> {
|
||||||
const importedVerses = await fetchJsonl<ImportedVerse>(
|
const importedVerses = await fetchJsonl<ImportedVerse>(
|
||||||
`${libreBibleDevRoot}/kjv-eng-kjv2006/verses.jsonl`,
|
`${libreBibleDevRoot}/kjv-eng-kjv2006/verses.jsonl`,
|
||||||
@@ -313,6 +374,74 @@ async function loadLibreBibleDevVerses(): Promise<Verse[]> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadBrowserCommentary(): Promise<Map<string, CommentaryEntry[]>> {
|
||||||
|
browserCommentaryCache ??= loadLibreBibleDevCommentary()
|
||||||
|
|
||||||
|
return browserCommentaryCache
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadLibreBibleDevCommentary(): Promise<Map<string, CommentaryEntry[]>> {
|
||||||
|
const importedEntries = await fetchJsonl<ImportedCommentaryEntry>(
|
||||||
|
`${libreBibleDevRoot}/matthew-henry-concise-ccel/commentary.jsonl`,
|
||||||
|
)
|
||||||
|
const commentary = new Map<string, CommentaryEntry[]>()
|
||||||
|
|
||||||
|
for (const entry of importedEntries) {
|
||||||
|
const mapped: CommentaryEntry = {
|
||||||
|
id: entry.id,
|
||||||
|
resourceId: entry.resource_id,
|
||||||
|
source: entry.source,
|
||||||
|
book: entry.book,
|
||||||
|
chapter: entry.chapter,
|
||||||
|
verseStart: entry.verse_start,
|
||||||
|
verseEnd: entry.verse_end,
|
||||||
|
reference: entry.reference,
|
||||||
|
title: entry.title,
|
||||||
|
text: entry.text,
|
||||||
|
}
|
||||||
|
const key = commentaryKey(mapped.book, mapped.chapter)
|
||||||
|
const current = commentary.get(key) ?? []
|
||||||
|
current.push(mapped)
|
||||||
|
commentary.set(key, current)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entries of commentary.values()) {
|
||||||
|
entries.sort((left, right) => left.verseStart - right.verseStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
return commentary
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBrowserDictionary(): Promise<Map<string, DictionaryEntry>> {
|
||||||
|
browserDictionaryCache ??= loadLibreBibleDevDictionary()
|
||||||
|
|
||||||
|
return browserDictionaryCache
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadLibreBibleDevDictionary(): Promise<Map<string, DictionaryEntry>> {
|
||||||
|
const importedEntries = await fetchJsonl<ImportedDictionaryEntry>(
|
||||||
|
`${libreBibleDevRoot}/easton-smith-dictionaries-neuu/entries.jsonl`,
|
||||||
|
)
|
||||||
|
return new Map(
|
||||||
|
importedEntries.map((entry) => [
|
||||||
|
normalizeTerm(entry.term),
|
||||||
|
{
|
||||||
|
id: entry.id,
|
||||||
|
resourceId: entry.resource_id,
|
||||||
|
term: entry.term,
|
||||||
|
slug: entry.slug,
|
||||||
|
definitions: entry.definitions.map((definition) => ({
|
||||||
|
source: definition.source,
|
||||||
|
sourceName: definition.source_name,
|
||||||
|
text: definition.text,
|
||||||
|
})),
|
||||||
|
scriptureRefs: entry.scripture_refs,
|
||||||
|
sources: entry.sources,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async function loadBrowserFootnotes(): Promise<Map<string, Footnote[]>> {
|
async function loadBrowserFootnotes(): Promise<Map<string, Footnote[]>> {
|
||||||
browserFootnotesCache ??= loadLibreBibleDevFootnotes()
|
browserFootnotesCache ??= loadLibreBibleDevFootnotes()
|
||||||
|
|
||||||
@@ -402,6 +531,14 @@ function browserVerseKey(verse: Verse) {
|
|||||||
return browserVerseSourceIds.get(verse.id) ?? ''
|
return browserVerseSourceIds.get(verse.id) ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function commentaryKey(book: string, chapter: number) {
|
||||||
|
return `${book}|${chapter}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTerm(term: string) {
|
||||||
|
return term.toLowerCase().replace(/[^a-z0-9\s'-]/g, '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchJsonl<T>(url: string): Promise<T[]> {
|
async function fetchJsonl<T>(url: string): Promise<T[]> {
|
||||||
const response = await fetch(url)
|
const response = await fetch(url)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export type Verse = {
|
|||||||
text: string
|
text: string
|
||||||
strongs: StrongLink[]
|
strongs: StrongLink[]
|
||||||
footnotes?: Footnote[]
|
footnotes?: Footnote[]
|
||||||
|
commentary?: CommentaryEntry[]
|
||||||
|
dictionary?: DictionaryEntry[]
|
||||||
tags: Tag[]
|
tags: Tag[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +36,38 @@ export type Footnote = {
|
|||||||
text: string
|
text: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CommentaryEntry = {
|
||||||
|
id: string
|
||||||
|
resourceId: string
|
||||||
|
source: string
|
||||||
|
book: string
|
||||||
|
chapter: number
|
||||||
|
verseStart: number
|
||||||
|
verseEnd: number
|
||||||
|
reference: string
|
||||||
|
title: string
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DictionaryDefinition = {
|
||||||
|
source: string
|
||||||
|
sourceName: string
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DictionaryEntry = {
|
||||||
|
id: string
|
||||||
|
resourceId: string
|
||||||
|
term: string
|
||||||
|
slug: string
|
||||||
|
definitions: DictionaryDefinition[]
|
||||||
|
scriptureRefs: Array<{
|
||||||
|
reference: string
|
||||||
|
original: string
|
||||||
|
}>
|
||||||
|
sources: string[]
|
||||||
|
}
|
||||||
|
|
||||||
export type LibraryState = {
|
export type LibraryState = {
|
||||||
verses: Verse[]
|
verses: Verse[]
|
||||||
tags: Tag[]
|
tags: Tag[]
|
||||||
|
|||||||
Reference in New Issue
Block a user