Add study data client adapter
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
- Recorded the active development direction: use the local HotView/hot-reload setup for iterative app work and defer web/local builds and hosted deployment unless explicitly requested.
|
- Recorded the active development direction: use the local HotView/hot-reload setup for iterative app work and defer web/local builds and hosted deployment unless explicitly requested.
|
||||||
|
- Started Phase 4 shared app core cleanup by adding an explicit `StudyDataClient` contract and browser/Tauri data adapters behind the existing study client API.
|
||||||
|
|
||||||
## 0.1.31 - 2026-07-15
|
## 0.1.31 - 2026-07-15
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -118,9 +118,11 @@ Definition of done:
|
|||||||
|
|
||||||
Goal: stop treating local preview, hosted web, and Tauri as separate code paths.
|
Goal: stop treating local preview, hosted web, and Tauri as separate code paths.
|
||||||
|
|
||||||
|
Current foundation note: `src/studyDataClient.ts` now defines the app-level resource/query contract, and `src/studyClient.ts` routes existing UI calls through browser and Tauri adapters. The browser adapter still owns the HotView JSON/JSONL package loading path, but the UI no longer needs to know which adapter is active.
|
||||||
|
|
||||||
Tasks:
|
Tasks:
|
||||||
|
|
||||||
- Extract a client/service interface for translations, pages, Strong's links, notes, commentaries, dictionaries, maps, audio, sessions, settings, and timelines.
|
- Extract a client/service interface for translations, pages, Strong's links, notes, commentaries, dictionaries, maps, audio, sessions, settings, and timelines. Started with `StudyDataClient` for resource/query behavior.
|
||||||
- Keep UI components pointed at that interface instead of hardcoding browser static paths or direct Tauri commands.
|
- Keep UI components pointed at that interface instead of hardcoding browser static paths or direct Tauri commands.
|
||||||
- Define the session schema once: active translation, page anchor, selected verse, enabled resources, visual settings, notes, timeline, and exports.
|
- Define the session schema once: active translation, page anchor, selected verse, enabled resources, visual settings, notes, timeline, and exports.
|
||||||
- Add a migration path from `localStorage` sessions to hosted persistence and Tauri SQLite persistence.
|
- Add a migration path from `localStorage` sessions to hosted persistence and Tauri SQLite persistence.
|
||||||
|
|||||||
+102
-36
@@ -1,6 +1,7 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import { buildVerseSearchIndex } from './searchIndex'
|
import { buildVerseSearchIndex } from './searchIndex'
|
||||||
|
import type { StudyDataClient, TranslationNotesByVerse } from './studyDataClient'
|
||||||
import type {
|
import type {
|
||||||
AudioBibleContext,
|
AudioBibleContext,
|
||||||
AudioBibleSegment,
|
AudioBibleSegment,
|
||||||
@@ -352,9 +353,7 @@ 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 getTranslations(): Promise<TranslationResource[]> {
|
async function getBrowserTranslations(): Promise<TranslationResource[]> {
|
||||||
if (inTauri()) return []
|
|
||||||
|
|
||||||
const catalog = await loadBrowserCatalog()
|
const catalog = await loadBrowserCatalog()
|
||||||
return catalog.resources
|
return catalog.resources
|
||||||
.filter((resource) => resource.resource_type === 'translation')
|
.filter((resource) => resource.resource_type === 'translation')
|
||||||
@@ -369,22 +368,18 @@ export async function getTranslations(): Promise<TranslationResource[]> {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLibraryState(translationId = defaultTranslationId): Promise<LibraryState> {
|
async function getBrowserLibraryState(translationId = defaultTranslationId): Promise<LibraryState> {
|
||||||
if (inTauri()) {
|
|
||||||
return invoke<LibraryState>('get_library_state')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
verses: await loadBrowserVersesForTranslation(translationId),
|
verses: await loadBrowserVersesForTranslation(translationId),
|
||||||
tags: allBrowserTags(),
|
tags: allBrowserTags(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTranslationNotesForVerses(
|
async function getBrowserTranslationNotesForVerses(
|
||||||
translationId: string,
|
translationId: string,
|
||||||
verses: Verse[],
|
verses: Verse[],
|
||||||
): Promise<Map<string, { footnotes: Footnote[]; studyNotes: Footnote[] }>> {
|
): Promise<TranslationNotesByVerse> {
|
||||||
if (inTauri() || verses.length === 0) return new Map()
|
if (verses.length === 0) return new Map()
|
||||||
|
|
||||||
const chapterKeys = [
|
const chapterKeys = [
|
||||||
...new Set(
|
...new Set(
|
||||||
@@ -411,25 +406,17 @@ export async function getTranslationNotesForVerses(
|
|||||||
return notesByVerse
|
return notesByVerse
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function searchVerses(query: string, translationId = defaultTranslationId): Promise<Verse[]> {
|
async function searchBrowserVerses(query: string, translationId = defaultTranslationId): Promise<Verse[]> {
|
||||||
if (inTauri()) {
|
|
||||||
return invoke<Verse[]>('search_verses', { query })
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchIndex = await loadBrowserSearchIndex(translationId)
|
const searchIndex = await loadBrowserSearchIndex(translationId)
|
||||||
return searchIndex.search(query)
|
return searchIndex.search(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addTagToVerse(
|
async function addBrowserTagToVerse(
|
||||||
verseId: number,
|
verseId: number,
|
||||||
label: string,
|
label: string,
|
||||||
color: string,
|
color: string,
|
||||||
translationId = defaultTranslationId,
|
translationId = defaultTranslationId,
|
||||||
) {
|
) {
|
||||||
if (inTauri()) {
|
|
||||||
return invoke<Verse>('add_tag_to_verse', { verseId, label, color })
|
|
||||||
}
|
|
||||||
|
|
||||||
const tagsByVerse = readBrowserTags()
|
const tagsByVerse = readBrowserTags()
|
||||||
const current = tagsByVerse[verseId] ?? []
|
const current = tagsByVerse[verseId] ?? []
|
||||||
const existing = current.find((tag) => tag.label.toLowerCase() === label.toLowerCase())
|
const existing = current.find((tag) => tag.label.toLowerCase() === label.toLowerCase())
|
||||||
@@ -443,9 +430,7 @@ export async function addTagToVerse(
|
|||||||
return verse
|
return verse
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCommentaryForVerse(verse: Verse): Promise<CommentaryEntry[]> {
|
async function getBrowserCommentaryForVerse(verse: Verse): Promise<CommentaryEntry[]> {
|
||||||
if (inTauri()) return []
|
|
||||||
|
|
||||||
const commentary = await loadBrowserCommentary()
|
const commentary = await loadBrowserCommentary()
|
||||||
const chapterEntries = commentary.get(commentaryKey(verse.book, verse.chapter)) ?? []
|
const chapterEntries = commentary.get(commentaryKey(verse.book, verse.chapter)) ?? []
|
||||||
return chapterEntries.filter(
|
return chapterEntries.filter(
|
||||||
@@ -453,9 +438,7 @@ export async function getCommentaryForVerse(verse: Verse): Promise<CommentaryEnt
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDictionaryEntriesForTerms(terms: string[]): Promise<DictionaryEntry[]> {
|
async function getBrowserDictionaryEntriesForTerms(terms: string[]): Promise<DictionaryEntry[]> {
|
||||||
if (inTauri()) return []
|
|
||||||
|
|
||||||
const dictionary = await loadBrowserDictionary()
|
const dictionary = await loadBrowserDictionary()
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
const entries: DictionaryEntry[] = []
|
const entries: DictionaryEntry[] = []
|
||||||
@@ -472,14 +455,10 @@ export async function getDictionaryEntriesForTerms(terms: string[]): Promise<Dic
|
|||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMapContextForVerses(
|
async function getBrowserMapContextForVerses(
|
||||||
selectedVerse: Verse,
|
selectedVerse: Verse,
|
||||||
pageVerses: Verse[],
|
pageVerses: Verse[],
|
||||||
): Promise<MapContext> {
|
): Promise<MapContext> {
|
||||||
if (inTauri()) {
|
|
||||||
return { selectedPlaces: [], pagePlaces: [], relatedRoutes: [] }
|
|
||||||
}
|
|
||||||
|
|
||||||
const versePlaces = await loadBrowserVersePlaces()
|
const versePlaces = await loadBrowserVersePlaces()
|
||||||
const selectedVerseId = browserVerseKey(selectedVerse)
|
const selectedVerseId = browserVerseKey(selectedVerse)
|
||||||
const pageVerseIds = new Set(pageVerses.map((verse) => browserVerseKey(verse)).filter(Boolean))
|
const pageVerseIds = new Set(pageVerses.map((verse) => browserVerseKey(verse)).filter(Boolean))
|
||||||
@@ -496,12 +475,10 @@ export async function getMapContextForVerses(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAudioContextForVerse(
|
async function getBrowserAudioContextForVerse(
|
||||||
translationId: string,
|
translationId: string,
|
||||||
verse: Verse,
|
verse: Verse,
|
||||||
): Promise<AudioBibleContext | null> {
|
): Promise<AudioBibleContext | null> {
|
||||||
if (inTauri()) return null
|
|
||||||
|
|
||||||
const catalog = await loadBrowserCatalog()
|
const catalog = await loadBrowserCatalog()
|
||||||
const resource = catalog.resources.find(
|
const resource = catalog.resources.find(
|
||||||
(item) =>
|
(item) =>
|
||||||
@@ -547,7 +524,7 @@ export async function getAudioContextForVerse(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAudioObjectUrl(segment: AudioBibleSegment): Promise<string> {
|
async function getBrowserAudioObjectUrl(segment: AudioBibleSegment): Promise<string> {
|
||||||
const cacheKey = `${segment.audioZipUrl}#${segment.audioEntryPath}`
|
const cacheKey = `${segment.audioZipUrl}#${segment.audioEntryPath}`
|
||||||
if (!browserAudioObjectUrlCache.has(cacheKey)) {
|
if (!browserAudioObjectUrlCache.has(cacheKey)) {
|
||||||
browserAudioObjectUrlCache.set(
|
browserAudioObjectUrlCache.set(
|
||||||
@@ -566,6 +543,95 @@ export async function getAudioObjectUrl(segment: AudioBibleSegment): Promise<str
|
|||||||
return browserAudioObjectUrlCache.get(cacheKey)!
|
return browserAudioObjectUrlCache.get(cacheKey)!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emptyMapContext: MapContext = {
|
||||||
|
selectedPlaces: [],
|
||||||
|
pagePlaces: [],
|
||||||
|
relatedRoutes: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const browserStudyDataClient: StudyDataClient = {
|
||||||
|
getTranslations: getBrowserTranslations,
|
||||||
|
getLibraryState: getBrowserLibraryState,
|
||||||
|
getTranslationNotesForVerses: getBrowserTranslationNotesForVerses,
|
||||||
|
searchVerses: searchBrowserVerses,
|
||||||
|
addTagToVerse: addBrowserTagToVerse,
|
||||||
|
getCommentaryForVerse: getBrowserCommentaryForVerse,
|
||||||
|
getDictionaryEntriesForTerms: getBrowserDictionaryEntriesForTerms,
|
||||||
|
getMapContextForVerses: getBrowserMapContextForVerses,
|
||||||
|
getAudioContextForVerse: getBrowserAudioContextForVerse,
|
||||||
|
getAudioObjectUrl: getBrowserAudioObjectUrl,
|
||||||
|
}
|
||||||
|
|
||||||
|
const tauriStudyDataClient: StudyDataClient = {
|
||||||
|
getTranslations: async () => [],
|
||||||
|
getLibraryState: () => invoke<LibraryState>('get_library_state'),
|
||||||
|
getTranslationNotesForVerses: async () => new Map(),
|
||||||
|
searchVerses: (query) => invoke<Verse[]>('search_verses', { query }),
|
||||||
|
addTagToVerse: (verseId, label, color) => invoke<Verse>('add_tag_to_verse', { verseId, label, color }),
|
||||||
|
getCommentaryForVerse: async () => [],
|
||||||
|
getDictionaryEntriesForTerms: async () => [],
|
||||||
|
getMapContextForVerses: async () => emptyMapContext,
|
||||||
|
getAudioContextForVerse: async () => null,
|
||||||
|
getAudioObjectUrl: async () => {
|
||||||
|
throw new Error('Audio object URLs are not available in the Tauri data adapter yet.')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStudyDataClient(): StudyDataClient {
|
||||||
|
return inTauri() ? tauriStudyDataClient : browserStudyDataClient
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTranslations(): Promise<TranslationResource[]> {
|
||||||
|
return getStudyDataClient().getTranslations()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLibraryState(translationId = defaultTranslationId): Promise<LibraryState> {
|
||||||
|
return getStudyDataClient().getLibraryState(translationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTranslationNotesForVerses(
|
||||||
|
translationId: string,
|
||||||
|
verses: Verse[],
|
||||||
|
): Promise<TranslationNotesByVerse> {
|
||||||
|
return getStudyDataClient().getTranslationNotesForVerses(translationId, verses)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function searchVerses(query: string, translationId = defaultTranslationId): Promise<Verse[]> {
|
||||||
|
return getStudyDataClient().searchVerses(query, translationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addTagToVerse(
|
||||||
|
verseId: number,
|
||||||
|
label: string,
|
||||||
|
color: string,
|
||||||
|
translationId = defaultTranslationId,
|
||||||
|
) {
|
||||||
|
return getStudyDataClient().addTagToVerse(verseId, label, color, translationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCommentaryForVerse(verse: Verse): Promise<CommentaryEntry[]> {
|
||||||
|
return getStudyDataClient().getCommentaryForVerse(verse)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDictionaryEntriesForTerms(terms: string[]): Promise<DictionaryEntry[]> {
|
||||||
|
return getStudyDataClient().getDictionaryEntriesForTerms(terms)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMapContextForVerses(selectedVerse: Verse, pageVerses: Verse[]): Promise<MapContext> {
|
||||||
|
return getStudyDataClient().getMapContextForVerses(selectedVerse, pageVerses)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAudioContextForVerse(
|
||||||
|
translationId: string,
|
||||||
|
verse: Verse,
|
||||||
|
): Promise<AudioBibleContext | null> {
|
||||||
|
return getStudyDataClient().getAudioContextForVerse(translationId, verse)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAudioObjectUrl(segment: AudioBibleSegment): Promise<string> {
|
||||||
|
return getStudyDataClient().getAudioObjectUrl(segment)
|
||||||
|
}
|
||||||
|
|
||||||
async function loadBrowserCatalog(): Promise<ImportedCatalog> {
|
async function loadBrowserCatalog(): Promise<ImportedCatalog> {
|
||||||
browserCatalogCache ??= fetchJson<ImportedCatalog>(`${libreBibleDevRoot}/catalog.json`)
|
browserCatalogCache ??= fetchJson<ImportedCatalog>(`${libreBibleDevRoot}/catalog.json`)
|
||||||
return browserCatalogCache
|
return browserCatalogCache
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import type {
|
||||||
|
AudioBibleContext,
|
||||||
|
AudioBibleSegment,
|
||||||
|
CommentaryEntry,
|
||||||
|
DictionaryEntry,
|
||||||
|
Footnote,
|
||||||
|
LibraryState,
|
||||||
|
MapContext,
|
||||||
|
TranslationResource,
|
||||||
|
Verse,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
export type TranslationNotesByVerse = Map<string, { footnotes: Footnote[]; studyNotes: Footnote[] }>
|
||||||
|
|
||||||
|
export type StudyDataClient = {
|
||||||
|
getTranslations(): Promise<TranslationResource[]>
|
||||||
|
getLibraryState(translationId?: string): Promise<LibraryState>
|
||||||
|
getTranslationNotesForVerses(translationId: string, verses: Verse[]): Promise<TranslationNotesByVerse>
|
||||||
|
searchVerses(query: string, translationId?: string): Promise<Verse[]>
|
||||||
|
addTagToVerse(verseId: number, label: string, color: string, translationId?: string): Promise<Verse>
|
||||||
|
getCommentaryForVerse(verse: Verse): Promise<CommentaryEntry[]>
|
||||||
|
getDictionaryEntriesForTerms(terms: string[]): Promise<DictionaryEntry[]>
|
||||||
|
getMapContextForVerses(selectedVerse: Verse, pageVerses: Verse[]): Promise<MapContext>
|
||||||
|
getAudioContextForVerse(translationId: string, verse: Verse): Promise<AudioBibleContext | null>
|
||||||
|
getAudioObjectUrl(segment: AudioBibleSegment): Promise<string>
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user