From 27f3d12b4e7d9f577f6968621d9f9f535d820109 Mon Sep 17 00:00:00 2001 From: TheNonPirate Date: Thu, 11 Jun 2026 12:08:15 -0600 Subject: [PATCH 1/8] Initial work on audio export Currently it downloads rather than sharing the audio. --- src/lib/components/ShareSelector.svelte | 182 ++++++++++++++++++ .../components/TextSelectionToolbar.svelte | 31 +-- src/lib/components/VerseOnImage.svelte | 2 +- src/lib/data/stores/view.ts | 4 +- src/routes/+layout.svelte | 6 + 5 files changed, 209 insertions(+), 16 deletions(-) create mode 100644 src/lib/components/ShareSelector.svelte diff --git a/src/lib/components/ShareSelector.svelte b/src/lib/components/ShareSelector.svelte new file mode 100644 index 000000000..ff5db9c2a --- /dev/null +++ b/src/lib/components/ShareSelector.svelte @@ -0,0 +1,182 @@ + + + + + + +
+ + + +
+
diff --git a/src/lib/components/TextSelectionToolbar.svelte b/src/lib/components/TextSelectionToolbar.svelte index 37a4a2661..9490f4d8d 100644 --- a/src/lib/components/TextSelectionToolbar.svelte +++ b/src/lib/components/TextSelectionToolbar.svelte @@ -7,7 +7,6 @@ TODO: -> Share -> Play -> Play Repeat - -> Verse On Image - Add note dialog - Add highlight colors --> @@ -22,6 +21,8 @@ TODO: import { shareText } from '$lib/data/share'; import { audioActive, + modal, + ModalType, refs, s, selectedVerses, @@ -131,18 +132,22 @@ TODO: } async function shareSelectedText() { - const book = $selectedVerses[0].book; - const reference = selectedVerses.getCompositeReference(); - const text = await selectedVerses.getCompositeText(); - const bookCol = $selectedVerses[0].collection; - const fullBook = getBook({ collection: bookCol, book: book }); - const bookAbbrev = fullBook?.abbreviation ?? fullBook?.name; - shareText( - scriptureConfig.name ?? '', - scriptureConfig.name + '\n\n' + text + '\n' + reference, - book + '.txt' - ); - logShareContent('Text', bookCol, bookAbbrev ?? '', reference); + if ($refs.hasAudio?.timingFile) { + modal.open(ModalType.Share); + } else { + const book = $selectedVerses[0].book; + const reference = selectedVerses.getCompositeReference(); + const text = await selectedVerses.getCompositeText(); + const bookCol = $selectedVerses[0].collection; + const fullBook = getBook({ collection: bookCol, book: book }); + const bookAbbrev = fullBook?.abbreviation ?? fullBook?.name; + shareText( + scriptureConfig.name ?? '', + scriptureConfig.name + '\n\n' + text + '\n' + reference, + book + '.txt' + ); + logShareContent('Text', bookCol, bookAbbrev ?? '', reference); + } } const backgroundColor = $derived($s['ui.bar.text-select']['background-color']); diff --git a/src/lib/components/VerseOnImage.svelte b/src/lib/components/VerseOnImage.svelte index 4f457f1e6..7244af32c 100644 --- a/src/lib/components/VerseOnImage.svelte +++ b/src/lib/components/VerseOnImage.svelte @@ -647,7 +647,7 @@ The verse on image component. downloadProgress = 0; await audioCtx?.close(); } - } //Most of this is AI-generated, so serious testing is needed. I've done a lot of testing, but it would be good to make sure there aren't subtle problems with this code. + } // EditorTabs centering feature: diff --git a/src/lib/data/stores/view.ts b/src/lib/data/stores/view.ts index c0f23db52..30e1d9393 100644 --- a/src/lib/data/stores/view.ts +++ b/src/lib/data/stores/view.ts @@ -32,8 +32,8 @@ export const ModalType = { Font: 'font', StopPlan: 'stop-plan', PlaybackSpeed: 'playback-speed', - VerseOnImage: 'verse-on-image', - Download: 'download' + Download: 'download', + Share: 'share' } as const; export type ModalType = (typeof ModalType)[keyof typeof ModalType]; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 1b5c7e6b4..dad15e0ab 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -9,6 +9,7 @@ import FontSelector from '$lib/components/FontSelector.svelte'; import NoteDialog from '$lib/components/NoteDialog.svelte'; import PlanStopDialog from '$lib/components/PlanStopDialog.svelte'; + import ShareSelector from '$lib/components/ShareSelector.svelte'; import Sidebar from '$lib/components/Sidebar.svelte'; import TextAppearanceSelector from '$lib/components/TextAppearanceSelector.svelte'; import catalog from '$lib/data/catalogData'; @@ -89,6 +90,9 @@ case ModalType.PlaybackSpeed: audioPlaybackSpeed?.showModal(); break; + case ModalType.Share: + shareSelector?.showModal(); + break; } }); modal.clear(); @@ -108,6 +112,7 @@ let textAppearanceSelector: TextAppearanceSelector | undefined = $state(); let collectionSelector: CollectionSelector | undefined = $state(); let fontSelector: FontSelector | undefined = $state(); + let shareSelector: ShareSelector | undefined = $state(); let noteDialog: NoteDialog | undefined = $state(); let planStopDialog: PlanStopDialog | undefined = $state(undefined); let planStopId: string = $state(''); @@ -156,6 +161,7 @@ /> + From 9fd21191e9be16f0e8bf8c8f5336695a643bc4c0 Mon Sep 17 00:00:00 2001 From: TheNonPirate Date: Thu, 11 Jun 2026 15:30:33 -0600 Subject: [PATCH 2/8] Caused it to share rather than downloading audio Audio will be downloaded if sharing fails, which occurs on most browsers. --- src/lib/components/ShareSelector.svelte | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/lib/components/ShareSelector.svelte b/src/lib/components/ShareSelector.svelte index ff5db9c2a..82caf1bd4 100644 --- a/src/lib/components/ShareSelector.svelte +++ b/src/lib/components/ShareSelector.svelte @@ -109,6 +109,23 @@ A component for verse-on-image providing a dropdown where you can choose to down const file = new File([blob], filename, { type: useWebm ? 'audio/webm' : 'audio/mp4' }); + try { + if ( + navigator.share && + navigator.canShare && + navigator.canShare({ files: [file] }) + ) { + let verses = await selectedVerses.getCompositeText(); + const shareData: ShareData = { title: reference, text: verses, files: [file] }; + + await navigator.share(shareData); + return; + } + } catch (error) { + console.error('Error sharing: ', error); + } + + // if we're here, we failed to share, so we'll try to use the download link const url = URL.createObjectURL(file); const anchor = document.createElement('a'); From e70ca401eae6b7238983b836195282c8d023cd62 Mon Sep 17 00:00:00 2001 From: TheNonPirate Date: Thu, 11 Jun 2026 15:46:10 -0600 Subject: [PATCH 3/8] Prevent aborted share from causing download --- src/lib/components/ShareSelector.svelte | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/components/ShareSelector.svelte b/src/lib/components/ShareSelector.svelte index 82caf1bd4..76986feb5 100644 --- a/src/lib/components/ShareSelector.svelte +++ b/src/lib/components/ShareSelector.svelte @@ -122,6 +122,9 @@ A component for verse-on-image providing a dropdown where you can choose to down return; } } catch (error) { + if ((error as { name?: string })?.name === 'AbortError') { + return; // user intentionally dismissed native share UI + } console.error('Error sharing: ', error); } From cfc9ec726a06e33961216eb6c8ee555f590be560 Mon Sep 17 00:00:00 2001 From: TheNonPirate Date: Mon, 15 Jun 2026 11:20:45 -0600 Subject: [PATCH 4/8] Added wav audio as another audio export type --- src/lib/components/ShareSelector.svelte | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/lib/components/ShareSelector.svelte b/src/lib/components/ShareSelector.svelte index 76986feb5..d866fc947 100644 --- a/src/lib/components/ShareSelector.svelte +++ b/src/lib/components/ShareSelector.svelte @@ -17,6 +17,7 @@ A component for verse-on-image providing a dropdown where you can choose to down canEncodeAudio, Mp4OutputFormat, Output, + WavOutputFormat, WebMOutputFormat } from 'mediabunny'; import type { AudioEncodingConfig } from 'mediabunny'; @@ -42,9 +43,14 @@ A component for verse-on-image providing a dropdown where you can choose to down const audioCtx = new AudioContext(); try { const audioConfig: AudioEncodingConfig = await pickSupportedAudioConfig(); - const useWebm = audioConfig.codec === 'opus'; + const outputFormat = + audioConfig.codec === 'aac' + ? { name: 'mp4', format: new Mp4OutputFormat() } + : audioConfig.codec === 'opus' + ? { name: 'webm', format: new WebMOutputFormat() } + : { name: 'wav', format: new WavOutputFormat() }; const output = new Output({ - format: useWebm ? new WebMOutputFormat() : new Mp4OutputFormat(), + format: outputFormat.format, target: new BufferTarget() }); @@ -103,11 +109,11 @@ A component for verse-on-image providing a dropdown where you can choose to down const buffer = output.target.buffer as BlobPart; const blob = new Blob([buffer], { - type: useWebm ? 'audio/webm' : 'audio/mp4' + type: 'audio/' + outputFormat.name }); - const filename = reference + (useWebm ? '.webm' : '.mp4'); + const filename = reference + '.' + outputFormat.name; const file = new File([blob], filename, { - type: useWebm ? 'audio/webm' : 'audio/mp4' + type: 'audio/' + outputFormat.name }); try { if ( @@ -151,7 +157,10 @@ A component for verse-on-image providing a dropdown where you can choose to down { codec: 'opus', bitrate: 96000 - } + }, + { codec: 'pcm-f32' }, + { codec: 'pcm-s24' }, + { codec: 'pcm-s16' } ]; for (const cfg of candidates) { @@ -160,7 +169,7 @@ A component for verse-on-image providing a dropdown where you can choose to down } } - throw new Error('No supported AAC configuration found.'); + throw new Error('No supported audio configuration found.'); } //This is used to determine a supported audio configuration. It first tries AAC, but then falls back to opus if AAC isn't supported. This is a duplicate of the function with the same name in VerseOnImage.svelte, so maybe it should be moved to somewhere that exports it for any place that needs it to use it? let modalId = 'shareSelector'; let modalThis: Modal; From 601244c44d0080a6581255ad18198ce88d60f642 Mon Sep 17 00:00:00 2001 From: TheNonPirate Date: Mon, 15 Jun 2026 12:27:33 -0600 Subject: [PATCH 5/8] Fixed shared verse reference not updating --- src/lib/components/ShareSelector.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/components/ShareSelector.svelte b/src/lib/components/ShareSelector.svelte index d866fc947..b220d99e3 100644 --- a/src/lib/components/ShareSelector.svelte +++ b/src/lib/components/ShareSelector.svelte @@ -24,7 +24,6 @@ A component for verse-on-image providing a dropdown where you can choose to down import Modal from './Modal.svelte'; let { vertOffset = '1rem' } = $props(); - const reference = $derived(selectedVerses.getCompositeReference()); async function shareSelectedText() { const book = $selectedVerses[0].book; const reference = selectedVerses.getCompositeReference(); @@ -40,6 +39,7 @@ A component for verse-on-image providing a dropdown where you can choose to down logShareContent('Text', bookCol, bookAbbrev ?? '', reference); } async function shareAudio() { + const reference = selectedVerses.getCompositeReference(); const audioCtx = new AudioContext(); try { const audioConfig: AudioEncodingConfig = await pickSupportedAudioConfig(); @@ -148,7 +148,7 @@ A component for verse-on-image providing a dropdown where you can choose to down } finally { await audioCtx?.close(); } - } //I'll need to have it share it rather than downloading it. + } async function pickSupportedAudioConfig() { const candidates: AudioEncodingConfig[] = [ { codec: 'aac', bitrate: 128000 }, From b5bf02bde4057744bb9f26463030b8991aea4558 Mon Sep 17 00:00:00 2001 From: TheNonPirate Date: Tue, 16 Jun 2026 12:15:11 -0600 Subject: [PATCH 6/8] Update ShareSelector description to be correct --- src/lib/components/ShareSelector.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/ShareSelector.svelte b/src/lib/components/ShareSelector.svelte index b220d99e3..fa1c12f85 100644 --- a/src/lib/components/ShareSelector.svelte +++ b/src/lib/components/ShareSelector.svelte @@ -1,6 +1,6 @@