Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6a2e76c
Make Audio Download modal
TheNonPirate Jul 6, 2026
17e95d2
Create DB of downloaded audio files
TheNonPirate Jul 6, 2026
35fa5ef
Implement audio downloading
TheNonPirate Jul 7, 2026
37e661e
Change when it asks for download to match native
TheNonPirate Jul 7, 2026
ff66c98
Create audio download loading screen
TheNonPirate Jul 8, 2026
b4e8cd1
Minor tweaks
TheNonPirate Jul 8, 2026
50ab0ec
Switch to storing audio using service worker cache
TheNonPirate Jul 9, 2026
0c4d570
Revert "Switch to storing audio using service worker cache"
TheNonPirate Jul 9, 2026
dd121a2
Cause audio to not play while downloading
TheNonPirate Jul 9, 2026
e05d7e4
Fix error with audioclips DB naming
TheNonPirate Jul 9, 2026
b49bb11
Fix lint errors
TheNonPirate Jul 9, 2026
bce2349
Download audio as stream that can be cancelled
TheNonPirate Jul 13, 2026
e1999fd
Fix TypeScript error
TheNonPirate Jul 13, 2026
dbd302a
Minor fixes
TheNonPirate Jul 13, 2026
a46d9d3
Fix Modal styling bug
TheNonPirate Jul 14, 2026
d0fdb85
Various tweaks
TheNonPirate Jul 14, 2026
619bd4c
Update AudioDownloadModal styling
TheNonPirate Jul 14, 2026
72c8ecc
Move audio clips DB into a separate file
TheNonPirate Jul 14, 2026
f27690c
Replace audio URL map with MRUCache
TheNonPirate Jul 16, 2026
74c1ff1
Move checkAudioAvailability to audio.ts
TheNonPirate Jul 16, 2026
eaf96d4
Minor lint fix
TheNonPirate Jul 16, 2026
79db6c3
Implement Bible Brain audio download
TheNonPirate Jul 16, 2026
cd6a377
Minor tweaks
TheNonPirate Jul 16, 2026
5358dc3
Fix bug with audio stuck playing wrong chapter
TheNonPirate Jul 17, 2026
f63f92d
Minor tweak
TheNonPirate Jul 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions convert/convertConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
18 changes: 16 additions & 2 deletions src/lib/components/AudioBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ TODO:
import PlayButton from './PlayButton.svelte';
import RepeatButton from './RepeatButton.svelte';

interface Props {
checkAudioAvailability?: () => Promise<boolean>; //checkAudioAvailability downloads the audio if necessary and returns true if the audio is available
}

let { checkAudioAvailability = () => Promise.resolve(true) }: Props = $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.
Expand Down Expand Up @@ -137,7 +142,16 @@ TODO:
<AudioIcon.Replay10 color={iconColor} />
</button>
{/if}
<PlayButton state={playButtonState} color={iconPlayColor} onclick={playPause} />
<PlayButton
state={playButtonState}
color={iconPlayColor}
onclick={() =>
checkAudioAvailability().then((audioAvailable) => {
if (audioAvailable) {
playPause();
}
})}
/>

{#if $refs.hasAudio?.timingFile}
<button class="audio-control-buttons" onclick={() => changeVerse(1)}>
Expand All @@ -160,7 +174,7 @@ TODO:
<AudioIcon.Speed color={iconColor} />
</button>
{/if}
{#if !$refs.hasAudio.timingFile}
{#if !$refs.hasAudio?.timingFile}
<!-- Progress Bar -->
<div class="audio-progress-value text-sm">
{format($audioPlayer.progress)}
Expand Down
157 changes: 157 additions & 0 deletions src/lib/components/AudioDownloadModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<!--
@component
Audio Download Modal Dialog component.
-->

<script lang="ts">
import { updateAudioPlayer } from '$lib/data/audio';
import { addAudioClip } from '$lib/data/audioclipsDB';
import { refs, t, userSettings } from '$lib/data/stores';
import { CheckboxIcon, CheckboxOutlineIcon } from '$lib/icons';
import { tick } from 'svelte';
import Modal from './Modal.svelte';
const modalId = 'audioDownloadModal';
let modal: Modal | undefined = $state(undefined);
let downloadAutomatically: boolean = $state(false);
let audioUrl: string = '';
let error = $state('');
export function showModal(url: string) {
audioUrl = url;
modal?.showModal();
}
export async function downloadAudio(url: string) {
try {
if (downloadAutomatically) {
$userSettings['audio-auto-download'] = 'auto';
}
downloadProgress = 1;
abortController = new AbortController();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh. Not in this PR, but you may want to look into adding AbortController to some of the non-audio downloads (i.e. Verse-on-Image)

let addedAudioClip = await addAudioClip(
{
docSet: $refs.docSet,
collection: $refs.collection,
book: $refs.book,
chapter: $refs.chapter
},
url,
abortController,
(percent) => {
tick().then(() => (downloadProgress = percent));
}
);
downloadProgress = 0;
if (!addedAudioClip) {
return false;
}
updateAudioPlayer($refs);
return addedAudioClip;
} catch (err) {
console.error('Error downloading audio: ', err);
return false;
}
}
async function finishModal() {
const addedAudioClip = await downloadAudio(audioUrl);
if (!addedAudioClip && !abortController?.signal.aborted) {
error = 'Audio clip could not be downloaded';
setTimeout(() => {
error = '';
}, 2000);
return false;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
let downloadProgress = $state(0);
let abortController: AbortController | undefined = undefined;
let audioDownloadingMessage = $derived(
$t['Audio_Downloading']
.replace('%book', $refs.name || $refs.book)
.replace('%chapter', $refs.chapter)
);
</script>

<Modal bind:this={modal} id={modalId}>
<div id="container" class="message">
<div class="message-body" id="message-body">
<div class="message-header"></div>
<div class="message-title">
{$t['Audio_Download_Title']}
</div>
<div class="message-text">
{$t['Audio_Download_Confirm']}
</div>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="message-checkbox flex w-full"
onclick={() => {
downloadAutomatically = !downloadAutomatically;
}}
>
<div class="message-checkbox-left">
{#if downloadAutomatically}
<CheckboxIcon></CheckboxIcon>
{:else}
<CheckboxOutlineIcon></CheckboxOutlineIcon>
{/if}
</div>
<div class="message-checkbox-caption">{$t['Audio_Download_Auto']}</div>
</div>
</div>

<div class="left-0 dy-modal-action message-footer pointer-events-none">
<div class="message-buttons">
<button class="dy-btn message-button pointer-events-auto" id="no">
{$t['Button_No']}
</button>
Comment thread
TheNonPirate marked this conversation as resolved.
<button
class="dy-btn message-button pointer-events-auto"
id="yes"
onclick={() => finishModal()}
>
{$t['Button_Yes']}
</button>
</div>
</div>
</div>
</Modal>
{#if error}
<div class="absolute inset-0 flex items-center justify-center z-50 pointer-events-none">
<div
class="dy-modal-box overflow-y-visible relative opacity-100 text-red-500 text-center pointer-events-auto"
>
{error}
</div>
</div>
{/if}
{#if downloadProgress > 0}
<div class="fixed inset-0 bg-black/50 backdrop-blur-xs flex items-center justify-center z-50">
<div class="message" id="container">
<div class="w-70 md:w-100 message-body">
<div class="message-header h-5"></div>
<div class="message-title">{$t['Audio_Download_Title']}</div>
<div class="message-text">{audioDownloadingMessage}</div>

<div
class="message-progress"
style="padding-left: 20px; padding-right: 20px; padding-top: 20px; padding-bottom: 20px;"
>
<div class="w-full h-1 dy-progress bg-[#e4e4e4]">
<div class="h-full bg-black" style="width: {downloadProgress}%"></div>
</div>
</div>
</div>
<div class="flex justify-end">
<button
class="dy-btn dy-btn-sm message-button"
onclick={() => {
downloadProgress = 0;
abortController?.abort();
}}>{$t['Button_Cancel']}</button
>
</div>
</div>
</div>
{/if}
14 changes: 6 additions & 8 deletions src/lib/components/Modal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,7 @@ See https://daisyui.com/components/modal/#modal-that-closes-when-clicked-outside
dialog?: HTMLDialogElement;
}

let {
id,
children,
styling = `background-color:${$themeColors['DialogBackgroundColor']};`,
onclose,
dialog = $bindable()
}: Props = $props();
let { id, children, styling, onclose, dialog = $bindable() }: Props = $props();
/**
* This exported function allows buttons/labels
* in other divs to trigger the modal popup
Expand All @@ -43,7 +37,11 @@ See https://daisyui.com/components/modal/#modal-that-closes-when-clicked-outside
class="dy-modal cursor-pointer"
style:direction={$direction}
>
<form method="dialog" style={styling} class="dy-modal-box overflow-y-visible relative">
<form
method="dialog"
style={styling ?? `background-color:${$themeColors['DialogBackgroundColor']};`}
class="dy-modal-box overflow-y-visible relative"
>
{@render children?.()}
<!--This is the snippet for the popup's actual contents-->
</form>
Expand Down
Loading