From de383596c7e68690af389cb3aaf6351a81df7814 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 15 Jul 2026 16:13:51 -0500 Subject: [PATCH] Add study data client adapter --- CHANGELOG.md | 1 + ROADMAP.md | 4 +- src/studyClient.ts | 138 ++++++++++++++++++++++++++++++----------- src/studyDataClient.ts | 26 ++++++++ 4 files changed, 132 insertions(+), 37 deletions(-) create mode 100644 src/studyDataClient.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c934ab0..ed9d48a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 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. +- 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 diff --git a/ROADMAP.md b/ROADMAP.md index 7a5ec3c..630a222 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -118,9 +118,11 @@ Definition of done: 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: -- 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. - 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. diff --git a/src/studyClient.ts b/src/studyClient.ts index fab3a5a..80f4d51 100644 --- a/src/studyClient.ts +++ b/src/studyClient.ts @@ -1,6 +1,7 @@ import { invoke } from '@tauri-apps/api/core' import JSZip from 'jszip' import { buildVerseSearchIndex } from './searchIndex' +import type { StudyDataClient, TranslationNotesByVerse } from './studyDataClient' import type { AudioBibleContext, AudioBibleSegment, @@ -352,9 +353,7 @@ function allBrowserTags() { return [...unique.values()].sort((a, b) => a.label.localeCompare(b.label)) } -export async function getTranslations(): Promise { - if (inTauri()) return [] - +async function getBrowserTranslations(): Promise { const catalog = await loadBrowserCatalog() return catalog.resources .filter((resource) => resource.resource_type === 'translation') @@ -369,22 +368,18 @@ export async function getTranslations(): Promise { })) } -export async function getLibraryState(translationId = defaultTranslationId): Promise { - if (inTauri()) { - return invoke('get_library_state') - } - +async function getBrowserLibraryState(translationId = defaultTranslationId): Promise { return { verses: await loadBrowserVersesForTranslation(translationId), tags: allBrowserTags(), } } -export async function getTranslationNotesForVerses( +async function getBrowserTranslationNotesForVerses( translationId: string, verses: Verse[], -): Promise> { - if (inTauri() || verses.length === 0) return new Map() +): Promise { + if (verses.length === 0) return new Map() const chapterKeys = [ ...new Set( @@ -411,25 +406,17 @@ export async function getTranslationNotesForVerses( return notesByVerse } -export async function searchVerses(query: string, translationId = defaultTranslationId): Promise { - if (inTauri()) { - return invoke('search_verses', { query }) - } - +async function searchBrowserVerses(query: string, translationId = defaultTranslationId): Promise { const searchIndex = await loadBrowserSearchIndex(translationId) return searchIndex.search(query) } -export async function addTagToVerse( +async function addBrowserTagToVerse( verseId: number, label: string, color: string, translationId = defaultTranslationId, ) { - if (inTauri()) { - return invoke('add_tag_to_verse', { verseId, label, color }) - } - const tagsByVerse = readBrowserTags() const current = tagsByVerse[verseId] ?? [] const existing = current.find((tag) => tag.label.toLowerCase() === label.toLowerCase()) @@ -443,9 +430,7 @@ export async function addTagToVerse( return verse } -export async function getCommentaryForVerse(verse: Verse): Promise { - if (inTauri()) return [] - +async function getBrowserCommentaryForVerse(verse: Verse): Promise { const commentary = await loadBrowserCommentary() const chapterEntries = commentary.get(commentaryKey(verse.book, verse.chapter)) ?? [] return chapterEntries.filter( @@ -453,9 +438,7 @@ export async function getCommentaryForVerse(verse: Verse): Promise { - if (inTauri()) return [] - +async function getBrowserDictionaryEntriesForTerms(terms: string[]): Promise { const dictionary = await loadBrowserDictionary() const seen = new Set() const entries: DictionaryEntry[] = [] @@ -472,14 +455,10 @@ export async function getDictionaryEntriesForTerms(terms: string[]): Promise { - if (inTauri()) { - return { selectedPlaces: [], pagePlaces: [], relatedRoutes: [] } - } - const versePlaces = await loadBrowserVersePlaces() const selectedVerseId = browserVerseKey(selectedVerse) 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, verse: Verse, ): Promise { - if (inTauri()) return null - const catalog = await loadBrowserCatalog() const resource = catalog.resources.find( (item) => @@ -547,7 +524,7 @@ export async function getAudioContextForVerse( } } -export async function getAudioObjectUrl(segment: AudioBibleSegment): Promise { +async function getBrowserAudioObjectUrl(segment: AudioBibleSegment): Promise { const cacheKey = `${segment.audioZipUrl}#${segment.audioEntryPath}` if (!browserAudioObjectUrlCache.has(cacheKey)) { browserAudioObjectUrlCache.set( @@ -566,6 +543,95 @@ export async function getAudioObjectUrl(segment: AudioBibleSegment): Promise [], + getLibraryState: () => invoke('get_library_state'), + getTranslationNotesForVerses: async () => new Map(), + searchVerses: (query) => invoke('search_verses', { query }), + addTagToVerse: (verseId, label, color) => invoke('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 { + return getStudyDataClient().getTranslations() +} + +export function getLibraryState(translationId = defaultTranslationId): Promise { + return getStudyDataClient().getLibraryState(translationId) +} + +export function getTranslationNotesForVerses( + translationId: string, + verses: Verse[], +): Promise { + return getStudyDataClient().getTranslationNotesForVerses(translationId, verses) +} + +export function searchVerses(query: string, translationId = defaultTranslationId): Promise { + 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 { + return getStudyDataClient().getCommentaryForVerse(verse) +} + +export function getDictionaryEntriesForTerms(terms: string[]): Promise { + return getStudyDataClient().getDictionaryEntriesForTerms(terms) +} + +export function getMapContextForVerses(selectedVerse: Verse, pageVerses: Verse[]): Promise { + return getStudyDataClient().getMapContextForVerses(selectedVerse, pageVerses) +} + +export function getAudioContextForVerse( + translationId: string, + verse: Verse, +): Promise { + return getStudyDataClient().getAudioContextForVerse(translationId, verse) +} + +export function getAudioObjectUrl(segment: AudioBibleSegment): Promise { + return getStudyDataClient().getAudioObjectUrl(segment) +} + async function loadBrowserCatalog(): Promise { browserCatalogCache ??= fetchJson(`${libreBibleDevRoot}/catalog.json`) return browserCatalogCache diff --git a/src/studyDataClient.ts b/src/studyDataClient.ts new file mode 100644 index 0000000..bfba3fc --- /dev/null +++ b/src/studyDataClient.ts @@ -0,0 +1,26 @@ +import type { + AudioBibleContext, + AudioBibleSegment, + CommentaryEntry, + DictionaryEntry, + Footnote, + LibraryState, + MapContext, + TranslationResource, + Verse, +} from './types' + +export type TranslationNotesByVerse = Map + +export type StudyDataClient = { + getTranslations(): Promise + getLibraryState(translationId?: string): Promise + getTranslationNotesForVerses(translationId: string, verses: Verse[]): Promise + searchVerses(query: string, translationId?: string): Promise + addTagToVerse(verseId: number, label: string, color: string, translationId?: string): Promise + getCommentaryForVerse(verse: Verse): Promise + getDictionaryEntriesForTerms(terms: string[]): Promise + getMapContextForVerses(selectedVerse: Verse, pageVerses: Verse[]): Promise + getAudioContextForVerse(translationId: string, verse: Verse): Promise + getAudioObjectUrl(segment: AudioBibleSegment): Promise +}