Complete study resource depth

This commit is contained in:
2026-07-15 17:26:25 -05:00
parent b7a2c6d230
commit c438f62e52
14 changed files with 206 additions and 24 deletions
+9
View File
@@ -4,6 +4,15 @@
- No unreleased changes.
## 0.1.34 - 2026-07-15
- Completed Phase 7 study resource depth for the currently packaged LibreBible resources.
- Made map and audio Bible resources optional, collapsible, session-aware, and skipped when disabled.
- Added current-session and new-session switches for contextual maps and KJV audio Bible.
- Surfaced Strong's pronunciation metadata in the definition panel when the lexicon package provides it.
- Pinned the Vite HMR websocket host to `127.0.0.1` so the active HotView tab can receive updates when opened on that address.
- Documented the current resource behavior and the remaining blocker for licensed pronunciation audio packages.
## 0.1.33 - 2026-07-15
- Completed Phase 6 notes/export foundation for the active HotView workflow.
+10 -6
View File
@@ -55,7 +55,7 @@ Work one phase at a time. A helper, spike, or supporting infrastructure task for
- Phase 4: Shared app core. Complete.
- Phase 5: Storage and search foundation. Complete.
- Phase 6: Notes, exports, and optional sync. Complete.
- Phase 7: Study resource depth and polish.
- Phase 7: Study resource depth and polish. Complete.
Supporting infrastructure already landed but not currently active as a phase: hosted web preview, hosted login, and deploy scripting. These remain parked until the user explicitly asks for a release/deploy pass.
@@ -173,16 +173,20 @@ Definition of done:
## Phase 7: Study Resource Depth
Status: complete for the currently packaged LibreBible resources.
Goal: continue turning the page into a complete study workspace.
Current status: the study margin dynamically shows Strong's definitions, translation notes, Matthew Henry commentary, Easton/Smith dictionary entries, contextual place maps, related UBS route labels, KJV audio controls, and Strong's pronunciation text when the active packages provide it. Maps and audio are now optional, collapsible, session-aware resources. Licensed pronunciation audio clips are not packaged yet, so the app does not present a fake playback control for them.
Tasks:
- Add map support from LibreBible place/gazetteer packages.
- Show map availability when a visible verse mentions a known place.
- Add audio Bible support at chapter granularity first, then verse granularity when sources provide timestamps or verse files.
- Add pronunciation audio tied to English terms, Strong's numbers, lemmas, Greek, and Hebrew where licensing permits.
- Add map support from LibreBible place/gazetteer packages. Complete.
- Show map availability when a visible verse mentions a known place. Complete.
- Add audio Bible support at chapter granularity first, then verse granularity when sources provide timestamps or verse files. Complete for KJV chapter and verse-entry playback from the packaged source.
- Add pronunciation audio tied to English terms, Strong's numbers, lemmas, Greek, and Hebrew where licensing permits. Complete for available Strong's pronunciation text; playable clips remain blocked until a licensed pronunciation-audio package exists.
- Add more commentaries, dictionaries, lexicons, cross-references, and study-note packages.
- Keep every resource optional, collapsible, session-aware, and license-attributed.
- Keep every resource optional, collapsible, session-aware, and license-attributed. Complete for currently visible resource panels.
Definition of done:
+37
View File
@@ -0,0 +1,37 @@
# Study Resource Depth
Phase 7 makes the study margin behave like a resource-aware workspace for the LibreBible packages that are currently available to HotView.
## Current Resource Surface
- Strong's links load through the active translation and show lemma, transliteration, definition, language, and pronunciation text when the lexicon entry provides it.
- Translation footnotes and study notes hydrate by visible chapter instead of requiring whole-library note loading.
- Matthew Henry Concise commentary loads for the selected verse range.
- Easton and Smith dictionary entries load from selected verse terms, linked English surfaces, and Strong's lemma surfaces.
- Context maps load OpenBible place matches for the visible page and highlight places tied to the selected verse.
- Related UBS Project MARBLE routes are listed when route titles match visible-page places.
- KJV audio loads chapter playlists from the packaged Eliran Wong source and can play the selected verse, the full chapter, or from the selected verse forward.
## Session Behavior
Maps and audio are first-class session resources now. Existing and new sessions can enable or disable:
- Strong's links
- footnotes
- commentary
- dictionary
- maps
- audio Bible
- timeline recording
Disabled maps and audio do not load their package data in the background. The map and audio cards also persist collapsed state through the shared study settings snapshot.
HotView is expected to run on `127.0.0.1:5174` in the active local loop. Vite's HMR websocket is pinned to that host so live updates keep flowing when the browser tab uses the same address.
## Pronunciation Boundary
The current Strong's package includes pronunciation text for some entries, so the definition card can show that text. The data repo does not currently include licensed pronunciation audio files tied to Strong's numbers, lemmas, Greek, Hebrew, or English terms. Until that exists, the app should not show pronunciation playback controls.
## Next Resource Work
Future resource depth should add new LibreBible packages first, then wire them through the same app-level resource client. Good next candidates are cross-references, additional commentaries, additional lexicons, atlas images, and licensed pronunciation-audio metadata.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "libre-study",
"version": "0.1.33",
"version": "0.1.34",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "libre-study",
"version": "0.1.33",
"version": "0.1.34",
"dependencies": {
"@tauri-apps/api": "^2.11.1",
"jszip": "^3.10.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "libre-study",
"private": true,
"version": "0.1.33",
"version": "0.1.34",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
+1 -1
View File
@@ -77,7 +77,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
[[package]]
name = "app"
version = "0.1.33"
version = "0.1.34"
dependencies = [
"log",
"rusqlite",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "app"
version = "0.1.33"
version = "0.1.34"
description = "A Tauri App"
authors = ["you"]
license = ""
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "Libre Study",
"version": "0.1.33",
"version": "0.1.34",
"identifier": "org.librestudy.desktop",
"build": {
"frontendDist": "../dist",
+3 -1
View File
@@ -608,7 +608,8 @@ textarea:focus-visible {
.panel-header span,
.margin-reference,
.translit {
.translit,
.pronunciation {
color: #657061;
font-size: 13px;
line-height: 1.4;
@@ -1759,6 +1760,7 @@ textarea:focus-visible {
.theme-dark .panel-header span,
.theme-dark .margin-reference,
.theme-dark .translit,
.theme-dark .pronunciation,
.theme-dark .eyebrow,
.theme-dark .collapse-button,
.theme-dark .mini-settings,
+123 -11
View File
@@ -239,6 +239,8 @@ function App() {
const [newSessionStrongs, setNewSessionStrongs] = useState(true)
const [newSessionCommentary, setNewSessionCommentary] = useState(true)
const [newSessionDictionary, setNewSessionDictionary] = useState(true)
const [newSessionMaps, setNewSessionMaps] = useState(true)
const [newSessionAudio, setNewSessionAudio] = useState(true)
const [newSessionTimeline, setNewSessionTimeline] = useState(true)
const [exportFormat, setExportFormat] = useState<ExportFormat>('json')
const [importStatus, setImportStatus] = useState('')
@@ -256,6 +258,8 @@ function App() {
const [studyNotesCollapsed, setStudyNotesCollapsed] = useState(false)
const [commentaryCollapsed, setCommentaryCollapsed] = useState(false)
const [dictionaryCollapsed, setDictionaryCollapsed] = useState(false)
const [mapCollapsed, setMapCollapsed] = useState(false)
const [audioCollapsed, setAudioCollapsed] = useState(false)
const [showOriginalLine, setShowOriginalLine] = useState(true)
const [showLinkedLine, setShowLinkedLine] = useState(true)
const [showDefinitionPanel, setShowDefinitionPanel] = useState(true)
@@ -308,6 +312,8 @@ function App() {
setStudyNotesCollapsed(settings.studyNotesCollapsed ?? false)
setCommentaryCollapsed(settings.commentaryCollapsed ?? false)
setDictionaryCollapsed(settings.dictionaryCollapsed ?? false)
setMapCollapsed(settings.mapCollapsed ?? false)
setAudioCollapsed(settings.audioCollapsed ?? false)
setShowOriginalLine(settings.showOriginalLine ?? true)
setShowLinkedLine(settings.showLinkedLine ?? true)
setShowDefinitionPanel(settings.showDefinitionPanel ?? true)
@@ -455,6 +461,8 @@ function App() {
studyNotesCollapsed,
commentaryCollapsed,
dictionaryCollapsed,
mapCollapsed,
audioCollapsed,
showOriginalLine,
showLinkedLine,
showDefinitionPanel,
@@ -468,6 +476,8 @@ function App() {
commentaryCollapsed,
definitionCollapsed,
dictionaryCollapsed,
mapCollapsed,
audioCollapsed,
lemmaFontSize,
linkedCollapsed,
linkedFontSize,
@@ -721,6 +731,10 @@ function App() {
setMapContext({ selectedPlaces: [], pagePlaces: [], relatedRoutes: [] })
return
}
if (activeSession?.mapsEnabled === false) {
setMapContext({ selectedPlaces: [], pagePlaces: [], relatedRoutes: [] })
return
}
setMapResourcesLoading(true)
try {
@@ -737,7 +751,7 @@ function App() {
return () => {
cancelled = true
}
}, [pageVerses, selectedVerse])
}, [activeSession?.mapsEnabled, pageVerses, selectedVerse])
useEffect(() => {
let cancelled = false
@@ -747,6 +761,11 @@ function App() {
setAudioContext(null)
return
}
if (activeSession?.audioEnabled === false) {
setAudioContext(null)
stopAudio()
return
}
setAudioResourcesLoading(true)
try {
@@ -769,7 +788,7 @@ function App() {
return () => {
cancelled = true
}
}, [activeSession?.baseTranslation, selectedVerse])
}, [activeSession?.audioEnabled, activeSession?.baseTranslation, selectedVerse])
useEffect(() => {
if (!audioRef.current) return
@@ -1105,6 +1124,8 @@ function App() {
strongsEnabled: newSessionStrongs,
commentaryEnabled: newSessionCommentary,
dictionaryEnabled: newSessionDictionary,
mapsEnabled: newSessionMaps,
audioEnabled: newSessionAudio,
timelineEnabled: newSessionTimeline,
}
setSessions((current) => [nextSession, ...current])
@@ -1122,7 +1143,9 @@ function App() {
typeof patch.strongsEnabled === 'boolean' ||
typeof patch.footnotesEnabled === 'boolean' ||
typeof patch.commentaryEnabled === 'boolean' ||
typeof patch.dictionaryEnabled === 'boolean'
typeof patch.dictionaryEnabled === 'boolean' ||
typeof patch.mapsEnabled === 'boolean' ||
typeof patch.audioEnabled === 'boolean'
) {
recordTimeline({ type: 'setting', label: 'Updated session resources' }, patch)
}
@@ -1719,6 +1742,18 @@ function App() {
}
function renderMapSection() {
if (activeSession?.mapsEnabled === false) {
return (
<article className="resource-card map-card">
<header className="margin-section-header">
<button type="button" className="collapse-button">Context Map</button>
<span>Off</span>
</header>
<p className="resource-disabled">Maps are disabled for this session.</p>
</article>
)
}
const places = mapContext.pagePlaces
const selectedPlaces = mapContext.selectedPlaces
const routes = mapContext.relatedRoutes
@@ -1726,7 +1761,22 @@ function App() {
return (
<article className="resource-card map-card" aria-label="Contextual maps">
<header className="margin-section-header">
<header
className="margin-section-header"
role="button"
tabIndex={0}
onClick={() => {
setMapCollapsed((current) => !current)
recordDisplaySetting('Toggled context map accordion')
}}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
setMapCollapsed((current) => !current)
recordDisplaySetting('Toggled context map accordion')
}
}}
>
<button type="button" className="collapse-button">Context Map</button>
<span>
{places.length
@@ -1736,7 +1786,7 @@ function App() {
: 'None'}
</span>
</header>
{places.length ? (
{!mapCollapsed && places.length ? (
<>
<div className="context-map" aria-label="Visible-page place map">
<div className="map-grid" aria-hidden="true" />
@@ -1782,16 +1832,28 @@ function App() {
Places: OpenBible.info. Routes: UBS Project MARBLE. Coordinates are contextual and may be approximate.
</p>
</>
) : (
) : !mapCollapsed ? (
<p className="resource-disabled">
{mapResourcesLoading ? 'Loading contextual map data...' : 'No mapped places matched this page.'}
</p>
)}
) : null}
</article>
)
}
function renderAudioSection() {
if (activeSession?.audioEnabled === false) {
return (
<article className="resource-card audio-card">
<header className="margin-section-header">
<button type="button" className="collapse-button">Audio Bible</button>
<span>Off</span>
</header>
<p className="resource-disabled">Audio Bible is disabled for this session.</p>
</article>
)
}
const selectedSegment = audioContext?.selectedSegment ?? null
const chapterSegments = audioContext?.chapterPlaylist?.segments ?? []
const selectedIndex = selectedSegment
@@ -1800,7 +1862,22 @@ function App() {
return (
<article className="resource-card audio-card" aria-label="Audio Bible">
<header className="margin-section-header">
<header
className="margin-section-header"
role="button"
tabIndex={0}
onClick={() => {
setAudioCollapsed((current) => !current)
recordDisplaySetting('Toggled audio Bible accordion')
}}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
setAudioCollapsed((current) => !current)
recordDisplaySetting('Toggled audio Bible accordion')
}
}}
>
<button type="button" className="collapse-button">Audio Bible</button>
<span>
{audioResourcesLoading
@@ -1810,7 +1887,7 @@ function App() {
: 'None'}
</span>
</header>
{audioContext && selectedSegment ? (
{!audioCollapsed && audioContext && selectedSegment ? (
<>
<div className="audio-controls">
<button
@@ -1873,13 +1950,13 @@ function App() {
<p className="map-attribution">{audioContext.attribution}</p>
<audio ref={audioRef} preload="none" onEnded={handleAudioEnded} />
</>
) : (
) : !audioCollapsed ? (
<p className="resource-disabled">
{audioResourcesLoading
? 'Loading audio metadata...'
: 'No audio package is attached to this translation yet.'}
</p>
)}
) : null}
</article>
)
}
@@ -2023,6 +2100,9 @@ function App() {
</div>
<h4>{selectedStrongEntry.lemma}</h4>
<p className="translit">{selectedStrongEntry.transliteration}</p>
{selectedStrongEntry.pronunciation ? (
<p className="pronunciation">Pronunciation: {selectedStrongEntry.pronunciation}</p>
) : null}
<p>{selectedStrongEntry.definition}</p>
<button type="button" onClick={() => searchStrong(selectedStrongEntry.strong)}>
Show matching verses
@@ -2298,6 +2378,22 @@ function App() {
/>
Dictionary
</label>
<label>
<input
type="checkbox"
checked={activeSession?.mapsEnabled ?? true}
onChange={(event) => setActiveSessionOption({ mapsEnabled: event.target.checked })}
/>
Maps
</label>
<label>
<input
type="checkbox"
checked={activeSession?.audioEnabled ?? true}
onChange={(event) => setActiveSessionOption({ audioEnabled: event.target.checked })}
/>
Audio Bible
</label>
<label>
<input
type="checkbox"
@@ -2393,6 +2489,22 @@ function App() {
/>
Include dictionary
</label>
<label>
<input
type="checkbox"
checked={newSessionMaps}
onChange={(event) => setNewSessionMaps(event.target.checked)}
/>
Include maps
</label>
<label>
<input
type="checkbox"
checked={newSessionAudio}
onChange={(event) => setNewSessionAudio(event.target.checked)}
/>
Include audio
</label>
<label>
<input
type="checkbox"
+3
View File
@@ -165,6 +165,7 @@ type ImportedBibleRoute = {
type BrowserLexiconEntry = {
lemma: string
transliteration: string
pronunciation: string
definition: string
language: 'Hebrew' | 'Greek'
}
@@ -1158,6 +1159,7 @@ function mapBrowserLexicon(importedEntries: ImportedStrongEntry[]) {
{
lemma: entry.lemma ?? '',
transliteration: entry.transliteration ?? '',
pronunciation: entry.pronunciation ?? '',
definition: entry.strongs_definition ?? entry.kjv_definition ?? '',
language: entry.language === 'hebrew' ? 'Hebrew' : 'Greek',
},
@@ -1179,6 +1181,7 @@ function mapStrongLinks(
position: link.position,
lemma: entry?.lemma ?? link.surface,
transliteration: entry?.transliteration ?? '',
pronunciation: entry?.pronunciation,
definition: entry?.definition ?? '',
language: entry?.language ?? strongLanguage(link.strong),
}
+1
View File
@@ -18,6 +18,7 @@ export type StrongLink = {
position: number
lemma: string
transliteration: string
pronunciation?: string
definition: string
language: 'Greek' | 'Hebrew'
}
+10
View File
@@ -41,6 +41,8 @@ export type StudySession = {
strongsEnabled: boolean
commentaryEnabled: boolean
dictionaryEnabled: boolean
mapsEnabled: boolean
audioEnabled: boolean
timelineEnabled: boolean
createdAt: string
updatedAt: string
@@ -62,6 +64,8 @@ export type StudySettings = {
studyNotesCollapsed: boolean
commentaryCollapsed: boolean
dictionaryCollapsed: boolean
mapCollapsed: boolean
audioCollapsed: boolean
showOriginalLine: boolean
showLinkedLine: boolean
showDefinitionPanel: boolean
@@ -117,6 +121,8 @@ export function defaultStudySettings(): StudySettings {
studyNotesCollapsed: false,
commentaryCollapsed: false,
dictionaryCollapsed: false,
mapCollapsed: false,
audioCollapsed: false,
showOriginalLine: true,
showLinkedLine: true,
showDefinitionPanel: true,
@@ -143,6 +149,8 @@ export function createSession(title = 'Study Session', baseTranslation = default
strongsEnabled: true,
commentaryEnabled: true,
dictionaryEnabled: true,
mapsEnabled: true,
audioEnabled: true,
timelineEnabled: true,
createdAt: now,
updatedAt: now,
@@ -179,6 +187,8 @@ export function normalizeSessions(sessions?: StudySession[] | null): StudySessio
strongsEnabled: session.strongsEnabled ?? true,
commentaryEnabled: session.commentaryEnabled ?? true,
dictionaryEnabled: session.dictionaryEnabled ?? true,
mapsEnabled: session.mapsEnabled ?? true,
audioEnabled: session.audioEnabled ?? true,
timelineEnabled: session.timelineEnabled ?? true,
notes: normalizeSessionNotes(session),
}
+4
View File
@@ -6,6 +6,10 @@ export default defineConfig({
plugins: [react()],
server: {
allowedHosts: ['desktop'],
hmr: {
host: '127.0.0.1',
protocol: 'ws',
},
fs: {
allow: ['..', 'W:/libre-bible-data'],
},