From 6a2e76cba3ff27f68df8c4a1b67f473e5aebbfff Mon Sep 17 00:00:00 2001 From: TheNonPirate Date: Mon, 6 Jul 2026 12:28:27 -0600 Subject: [PATCH 01/25] Make Audio Download modal --- src/lib/components/AudioDownloadModal.svelte | 60 ++++++++++++++++++++ src/lib/data/audio.ts | 3 + src/lib/data/stores/view.ts | 3 +- src/routes/+layout.svelte | 6 ++ 4 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/lib/components/AudioDownloadModal.svelte diff --git a/src/lib/components/AudioDownloadModal.svelte b/src/lib/components/AudioDownloadModal.svelte new file mode 100644 index 000000000..7e81463b2 --- /dev/null +++ b/src/lib/components/AudioDownloadModal.svelte @@ -0,0 +1,60 @@ + + + + + +
+
+
+
+ {$t['Audio_Download_Title']} +
+
+ {$t['Audio_Download_Confirm']} +
+ + +
(downloadAutomatically = !downloadAutomatically)} + > +
+ {#if downloadAutomatically} + + {:else} + + {/if} +
+
{$t['Audio_Download_Auto']}
+
+
+ + +
+
diff --git a/src/lib/data/audio.ts b/src/lib/data/audio.ts index 1d253c821..9c06c70e1 100644 --- a/src/lib/data/audio.ts +++ b/src/lib/data/audio.ts @@ -6,6 +6,8 @@ import { audioPlayerDefault, audioPlayer as audioPlayerStore, defaultPlayMode, + modal, + ModalType, playMode, PlayMode, refs, @@ -710,6 +712,7 @@ export async function getAudioSourceInfo( } audioPath = audioSources[audioKey]; } else if (audioSource?.type === 'download') { + modal.open(ModalType.DownloadAudio); audioPath = pathJoin([audioSource.address, audio.filename]); } //parse timing file diff --git a/src/lib/data/stores/view.ts b/src/lib/data/stores/view.ts index da6240ea7..5f87da9af 100644 --- a/src/lib/data/stores/view.ts +++ b/src/lib/data/stores/view.ts @@ -33,7 +33,8 @@ export const ModalType = { PlaybackSpeed: 'playback-speed', VerseOnImage: 'verse-on-image', Download: 'download', - Collection: 'collection' + Collection: 'collection', + DownloadAudio: 'download-audio' } as const; export type ModalType = (typeof ModalType)[keyof typeof ModalType]; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 251968cc8..40cbc2ae0 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -4,6 +4,7 @@ import manifestHref from '$assets/manifestUrl.json'; import '$lib/styles/app.css'; import config from '$assets/config'; + import AudioDownloadModal from '$lib/components/AudioDownloadModal.svelte'; import AudioPlaybackSpeed from '$lib/components/AudioPlaybackSpeed.svelte'; import CollectionModal from '$lib/components/CollectionModal.svelte'; import FontSelector from '$lib/components/FontSelector.svelte'; @@ -83,6 +84,9 @@ planStopId = data as string; planStopDialog?.showModal(); break; + case ModalType.DownloadAudio: + audioDownloadModal?.showModal(); + break; case ModalType.PlaybackSpeed: audioPlaybackSpeed?.showModal(); break; @@ -112,6 +116,7 @@ let noteDialog: NoteDialog | undefined = $state(); let collectionModal: CollectionModal | undefined = $state(); let planStopDialog: PlanStopDialog | undefined = $state(undefined); + let audioDownloadModal: AudioDownloadModal | undefined = $state(); let planStopId: string = $state(''); let audioPlaybackSpeed: AudioPlaybackSpeed | undefined = $state(); @@ -161,6 +166,7 @@ bind:planId={planStopId} vertOffset={NAVBAR_HEIGHT} /> + From 17e95d213e74b0c1a56fbfe0b513e628a3789c7c Mon Sep 17 00:00:00 2001 From: TheNonPirate Date: Mon, 6 Jul 2026 14:07:29 -0600 Subject: [PATCH 02/25] Create DB of downloaded audio files --- src/lib/components/AudioDownloadModal.svelte | 21 ++++++- src/lib/data/audio.ts | 61 +++++++++++++++++++- src/routes/+layout.svelte | 3 +- 3 files changed, 80 insertions(+), 5 deletions(-) diff --git a/src/lib/components/AudioDownloadModal.svelte b/src/lib/components/AudioDownloadModal.svelte index 7e81463b2..153014e47 100644 --- a/src/lib/components/AudioDownloadModal.svelte +++ b/src/lib/components/AudioDownloadModal.svelte @@ -4,7 +4,8 @@ Plan Stop Modal Dialog component. --> @@ -51,7 +66,7 @@ Plan Stop Modal Dialog component. - diff --git a/src/lib/data/audio.ts b/src/lib/data/audio.ts index 9c06c70e1..fc183e5e4 100644 --- a/src/lib/data/audio.ts +++ b/src/lib/data/audio.ts @@ -18,6 +18,7 @@ import { } from '$lib/data/stores'; import { getBibleBrainUrl } from '$lib/scripts/mediaUtils'; import { pathJoin } from '$lib/scripts/stringUtils'; +import { openDB, type DBSchema } from 'idb'; import { logAudioDuration, logAudioPlay } from './analytics'; const audioSources = import.meta.glob('./*', { @@ -679,6 +680,64 @@ function getDamId(audioSource: AudioSource) { return damId; } +export interface AudioItem { + date: number; + docSet: string; + collection: string; + book: string; + chapter: string; + blob: Blob; +} +interface AudioClips extends DBSchema { + bookmarks: { + key: number; + value: AudioItem; + indexes: { + 'collection, book, chapter': string[]; + date: string; + }; + }; +} +let audioDB: Awaited>> | null = null; +async function openAudioClips() { + if (!audioDB) { + audioDB = await openDB('audioclips', 1, { + upgrade(db) { + const audioStore = db.createObjectStore('audioclips', { + keyPath: 'date' + }); + audioStore.createIndex('collection, book, chapter', [ + 'collection', + 'book', + 'chapter' + ]); + audioStore.createIndex('date', ['date']); + } + }); + } + return audioDB; +} +export async function addAudioClip( + item: { + docSet: string; + collection: string; + book: string; + chapter: string; + }, + url: string +) { + const response = await fetch(url); + const blob = await response.blob(); + const audioClips = await openAudioClips(); + const date = new Date()[Symbol.toPrimitive]('number'); + const bookIndex = scriptureConfig.bookCollections + ?.find((x) => x.id === item.collection) + ?.books.findIndex((x) => x.id === item.book); + if (bookIndex !== undefined && bookIndex >= 0) { + const nextItem = { ...item, date: date, blob: blob }; + await audioClips.add('audioclips', nextItem); + } +} export async function getAudioSourceInfo( item: Partial<{ collection: string; @@ -712,8 +771,8 @@ export async function getAudioSourceInfo( } audioPath = audioSources[audioKey]; } else if (audioSource?.type === 'download') { - modal.open(ModalType.DownloadAudio); audioPath = pathJoin([audioSource.address, audio.filename]); + modal.open(ModalType.DownloadAudio, audioPath); } //parse timing file const timing: Timing[] = []; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 40cbc2ae0..cdf742620 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -85,7 +85,8 @@ planStopDialog?.showModal(); break; case ModalType.DownloadAudio: - audioDownloadModal?.showModal(); + let audioUrl = data as string; + audioDownloadModal?.showModal(audioUrl); break; case ModalType.PlaybackSpeed: audioPlaybackSpeed?.showModal(); From 35fa5ef15443b0282d86f923c56cff3415e041c7 Mon Sep 17 00:00:00 2001 From: TheNonPirate Date: Tue, 7 Jul 2026 14:56:26 -0600 Subject: [PATCH 03/25] Implement audio downloading --- convert/convertConfig.ts | 4 +- src/lib/components/AudioDownloadModal.svelte | 39 +++++++++++--- src/lib/data/audio.ts | 55 +++++++++++++++----- 3 files changed, 76 insertions(+), 22 deletions(-) diff --git a/convert/convertConfig.ts b/convert/convertConfig.ts index cb37da3a4..67369369c 100644 --- a/convert/convertConfig.ts +++ b/convert/convertConfig.ts @@ -1876,10 +1876,8 @@ function filterFeaturesNotReady(data: ScriptureConfig | DictionaryConfig) { data.mainFeatures['settings-keep-screen-on'] = false; data.mainFeatures['settings-share-usage-data'] = false; - // Offline audio is not done - data.mainFeatures['settings-audio-access-method'] = false; + // Offline video is not done data.mainFeatures['settings-video-access-method'] = false; - data.mainFeatures['settings-audio-download-mode'] = false; /** * Keyboards are not supported (documentation links were added in 14.2) diff --git a/src/lib/components/AudioDownloadModal.svelte b/src/lib/components/AudioDownloadModal.svelte index 153014e47..4163c1848 100644 --- a/src/lib/components/AudioDownloadModal.svelte +++ b/src/lib/components/AudioDownloadModal.svelte @@ -5,7 +5,7 @@ Plan Stop Modal Dialog component. @@ -48,7 +57,10 @@ Plan Stop Modal Dialog component.
(downloadAutomatically = !downloadAutomatically)} + onclick={() => { + console.log('HI'); + downloadAutomatically = !downloadAutomatically; + }} >
{#if downloadAutomatically} @@ -61,15 +73,28 @@ Plan Stop Modal Dialog component.
-
+{#if error} +
+
+ {error} +
+
+{/if} diff --git a/src/lib/data/audio.ts b/src/lib/data/audio.ts index fc183e5e4..65a9c973e 100644 --- a/src/lib/data/audio.ts +++ b/src/lib/data/audio.ts @@ -11,6 +11,7 @@ import { playMode, PlayMode, refs, + userSettings, type AudioPlayer, type PlayModeRange, type PlayModeSettings, @@ -19,6 +20,7 @@ import { import { getBibleBrainUrl } from '$lib/scripts/mediaUtils'; import { pathJoin } from '$lib/scripts/stringUtils'; import { openDB, type DBSchema } from 'idb'; +import { get } from 'svelte/store'; import { logAudioDuration, logAudioPlay } from './analytics'; const audioSources = import.meta.glob('./*', { @@ -726,18 +728,33 @@ export async function addAudioClip( }, url: string ) { - const response = await fetch(url); - const blob = await response.blob(); + try { + const response = await fetch(url); + const blob = await response.blob(); + const audioClips = await openAudioClips(); + const date = new Date()[Symbol.toPrimitive]('number'); + const bookIndex = scriptureConfig.bookCollections + ?.find((x) => x.id === item.collection) + ?.books.findIndex((x) => x.id === item.book); + if (bookIndex !== undefined && bookIndex >= 0) { + const nextItem = { ...item, date: date, blob: blob }; + await audioClips.add('audioclips', nextItem); + } + return true; + } catch (error) { + console.error('Error downloading audio:', error); + return false; + } +} +export async function findAudioClip(item: { collection: string; book: string; chapter: string }) { const audioClips = await openAudioClips(); - const date = new Date()[Symbol.toPrimitive]('number'); - const bookIndex = scriptureConfig.bookCollections - ?.find((x) => x.id === item.collection) - ?.books.findIndex((x) => x.id === item.book); - if (bookIndex !== undefined && bookIndex >= 0) { - const nextItem = { ...item, date: date, blob: blob }; - await audioClips.add('audioclips', nextItem); - } + const tx = audioClips.transaction('audioclips', 'readonly'); + const index = tx.store.index('collection, book, chapter'); + const result = await index.getAll([item.collection, item.book, item.chapter]); + await tx.done; + return result[0]; } + export async function getAudioSourceInfo( item: Partial<{ collection: string; @@ -771,8 +788,22 @@ export async function getAudioSourceInfo( } audioPath = audioSources[audioKey]; } else if (audioSource?.type === 'download') { - audioPath = pathJoin([audioSource.address, audio.filename]); - modal.open(ModalType.DownloadAudio, audioPath); + audioPath = pathJoin([audioSource.address, audio.filename]); //I'll need to move this to when you try to open the audio bar or play the audio + if (get(userSettings)['audio-access-method'] === 'download') { + let foundAudioClip = await findAudioClip({ + collection: item.collection || '', + book: item.book || '', + chapter: item.chapter || '' + }); + if (foundAudioClip) { + audioPath = URL.createObjectURL(foundAudioClip.blob); + } else { + //I'll need to automatically download instead of using the modal if get(userSettings)['audio-auto-download'] is 'auto' + if (audioSource?.accessMethods?.includes('download')) { + modal.open(ModalType.DownloadAudio, audioPath); + } + } + } } //parse timing file const timing: Timing[] = []; From 37e661e5e8c795c2b87070ac7637e9fe518e5c00 Mon Sep 17 00:00:00 2001 From: TheNonPirate Date: Tue, 7 Jul 2026 16:27:29 -0600 Subject: [PATCH 04/25] Change when it asks for download to match native --- src/lib/components/AudioBar.svelte | 10 +++++- src/lib/components/AudioDownloadModal.svelte | 11 ++++-- src/lib/data/audio.ts | 26 +++++--------- src/routes/+layout.svelte | 8 +++-- src/routes/text/+page.svelte | 36 ++++++++++++++++++-- 5 files changed, 65 insertions(+), 26 deletions(-) diff --git a/src/lib/components/AudioBar.svelte b/src/lib/components/AudioBar.svelte index 2eeffebc4..7233bb5e1 100644 --- a/src/lib/components/AudioBar.svelte +++ b/src/lib/components/AudioBar.svelte @@ -32,6 +32,7 @@ TODO: import PlayButton from './PlayButton.svelte'; import RepeatButton from './RepeatButton.svelte'; + let { checkForAudioDownload = () => {} } = $props(); function mayResetPlayMode(hasTiming: boolean) { // If the current mode is repeatSelection and the reference is changed to something without timing // (even chapter without audio), then reset the playMode. This matches how the Android app behaves. @@ -137,7 +138,14 @@ TODO: {/if} - + { + checkForAudioDownload(); + playPause(); + }} + /> {#if $refs.hasAudio?.timingFile} diff --git a/src/lib/data/audio.ts b/src/lib/data/audio.ts index 65a9c973e..8272d1c05 100644 --- a/src/lib/data/audio.ts +++ b/src/lib/data/audio.ts @@ -6,12 +6,9 @@ import { audioPlayerDefault, audioPlayer as audioPlayerStore, defaultPlayMode, - modal, - ModalType, playMode, PlayMode, refs, - userSettings, type AudioPlayer, type PlayModeRange, type PlayModeSettings, @@ -788,21 +785,14 @@ export async function getAudioSourceInfo( } audioPath = audioSources[audioKey]; } else if (audioSource?.type === 'download') { - audioPath = pathJoin([audioSource.address, audio.filename]); //I'll need to move this to when you try to open the audio bar or play the audio - if (get(userSettings)['audio-access-method'] === 'download') { - let foundAudioClip = await findAudioClip({ - collection: item.collection || '', - book: item.book || '', - chapter: item.chapter || '' - }); - if (foundAudioClip) { - audioPath = URL.createObjectURL(foundAudioClip.blob); - } else { - //I'll need to automatically download instead of using the modal if get(userSettings)['audio-auto-download'] is 'auto' - if (audioSource?.accessMethods?.includes('download')) { - modal.open(ModalType.DownloadAudio, audioPath); - } - } + audioPath = pathJoin([audioSource.address, audio.filename]); + let foundAudioClip = await findAudioClip({ + collection: item.collection || '', + book: item.book || '', + chapter: item.chapter || '' + }); //If the audio has been downloaded already, use that. + if (foundAudioClip) { + audioPath = URL.createObjectURL(foundAudioClip.blob); } } //parse timing file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index cdf742620..a076e8eb1 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -85,8 +85,12 @@ planStopDialog?.showModal(); break; case ModalType.DownloadAudio: - let audioUrl = data as string; - audioDownloadModal?.showModal(audioUrl); + let audioModalData = data as { audioPath: string; show: boolean }; + if (audioModalData.show) { + audioDownloadModal?.showModal(audioModalData.audioPath); + } else { + audioDownloadModal?.downloadAudio(audioModalData.audioPath); + } break; case ModalType.PlaybackSpeed: audioPlaybackSpeed?.showModal(); diff --git a/src/routes/text/+page.svelte b/src/routes/text/+page.svelte index 2b84f46c9..7f27f4299 100644 --- a/src/routes/text/+page.svelte +++ b/src/routes/text/+page.svelte @@ -17,7 +17,7 @@ import StackView from '$lib/components/StackView.svelte'; import { showTextAppearance } from '$lib/components/TextAppearanceSelector.svelte'; import TextSelectionToolbar from '$lib/components/TextSelectionToolbar.svelte'; - import { playStop, seekToVerse, updateAudioPlayer } from '$lib/data/audio'; + import { findAudioClip, playStop, seekToVerse, updateAudioPlayer } from '$lib/data/audio'; import { actionBarColor, analytics, @@ -59,6 +59,7 @@ } from '$lib/icons'; import { navigateToTextChapterInDirection } from '$lib/navigate'; import { getFeatureValueBoolean, getFeatureValueString } from '$lib/scripts/configUtils'; + import { pathJoin } from '$lib/scripts/stringUtils'; import { resolve } from '$lib/utils/paths'; import { onDestroy, onMount } from 'svelte'; import { @@ -394,6 +395,34 @@ } } }; + export async function checkForAudioDownload() { + const audio = scriptureConfig.bookCollections + ?.find((c) => $refs.collection === c.id) + ?.books?.find((b) => b.id === $refs.book) + ?.audio?.find((a) => $refs.chapter === '' + a.num); + if (audio) { + const audioSource = scriptureConfig.audio?.sources[audio.src]; + if (audioSource?.type === 'download') { + let audioPath = pathJoin([audioSource.address, audio.filename]); + if ($userSettings['audio-access-method'] === 'download') { + let foundAudioClip = await findAudioClip({ + collection: $refs.collection || '', + book: $refs.book || '', + chapter: $refs.chapter || '' + }); + if (!foundAudioClip) { + if (audioSource?.accessMethods?.includes('download')) { + if ($userSettings['audio-auto-download'] === 'auto') { + modal.open(ModalType.DownloadAudio, { audioPath, show: false }); //Just download it without showing the modal + } else { + modal.open(ModalType.DownloadAudio, { audioPath, show: true }); + } + } + } + } + } + } + } $effect(() => { updateHighlight($audioHighlightElements, highlightColor); @@ -474,6 +503,9 @@ + + + +{/if} From b4e8cd17c1dfe2ae70b3c16156aaa51b5cbe6618 Mon Sep 17 00:00:00 2001 From: TheNonPirate Date: Wed, 8 Jul 2026 16:12:45 -0600 Subject: [PATCH 06/25] Minor tweaks --- src/lib/components/AudioDownloadModal.svelte | 55 +++++++++++++------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/src/lib/components/AudioDownloadModal.svelte b/src/lib/components/AudioDownloadModal.svelte index 3076a8a7c..1301bc906 100644 --- a/src/lib/components/AudioDownloadModal.svelte +++ b/src/lib/components/AudioDownloadModal.svelte @@ -1,10 +1,15 @@