From edd1ca9200e2e12aba13505ac2358b16619e3e16 Mon Sep 17 00:00:00 2001 From: TheNonPirate Date: Thu, 11 Jun 2026 12:08:15 -0600 Subject: [PATCH 01/12] Initial work on audio export Currently it downloads rather than sharing the audio. --- src/lib/components/ShareSelector.svelte | 182 ++++++++++++++++++ .../components/TextSelectionToolbar.svelte | 37 ++-- src/lib/components/VerseOnImage.svelte | 2 +- src/routes/+layout.svelte | 2 + 4 files changed, 206 insertions(+), 17 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 9b4670b93..cae8cf51f 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 --> @@ -21,6 +20,8 @@ TODO: import { shareText } from '$lib/data/share'; import { audioActive, + modal, + ModalType, refs, s, selectedVerses, @@ -136,21 +137,25 @@ 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; - const copyShareMessage = scriptureConfig.bookCollections?.find( - (x) => x.id === bookCol - )?.copyShareMessage; - shareText( - scriptureConfig.name ?? '', - text + '\n' + reference + (copyShareMessage ? '\n' + copyShareMessage : ''), - 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; + const copyShareMessage = scriptureConfig.bookCollections?.find( + (x) => x.id === bookCol + )?.copyShareMessage; + shareText( + scriptureConfig.name ?? '', + text + '\n' + reference + (copyShareMessage ? '\n' + copyShareMessage : ''), + 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 e8a22c975..880124d49 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/routes/+layout.svelte b/src/routes/+layout.svelte index 251968cc8..6af6d722f 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'; @@ -109,6 +110,7 @@ let textAppearanceSelector: TextAppearanceSelector | undefined = $state(); let fontSelector: FontSelector | undefined = $state(); + let shareSelector: ShareSelector | undefined = $state(); let noteDialog: NoteDialog | undefined = $state(); let collectionModal: CollectionModal | undefined = $state(); let planStopDialog: PlanStopDialog | undefined = $state(undefined); From 233b51f77c61936b3da27826f047dcc7e0ddc548 Mon Sep 17 00:00:00 2001 From: TheNonPirate Date: Thu, 11 Jun 2026 15:30:33 -0600 Subject: [PATCH 02/12] 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 d2c0001f4dad95822be6fdec5febbd146ea9f928 Mon Sep 17 00:00:00 2001 From: TheNonPirate Date: Thu, 11 Jun 2026 15:46:10 -0600 Subject: [PATCH 03/12] 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 22229e4f31073785aaba61afe7d2d7b7721a3796 Mon Sep 17 00:00:00 2001 From: TheNonPirate Date: Mon, 15 Jun 2026 11:20:45 -0600 Subject: [PATCH 04/12] 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 dcb114317387f572c56afb68ba5b7e964ba00b18 Mon Sep 17 00:00:00 2001 From: TheNonPirate Date: Mon, 15 Jun 2026 12:27:33 -0600 Subject: [PATCH 05/12] 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 44552dfc4410cb6a7b9c2399120be5363b804a87 Mon Sep 17 00:00:00 2001 From: TheNonPirate Date: Tue, 16 Jun 2026 12:15:11 -0600 Subject: [PATCH 06/12] 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 @@ - +
{@render children()} From 6ed6cb2bf0a9afe507c220b16066b39b8e1db035 Mon Sep 17 00:00:00 2001 From: TheNonPirate Date: Wed, 15 Jul 2026 12:43:51 -0600 Subject: [PATCH 11/12] Adjust and refactor modal styling --- src/lib/components/DownloadSelector.svelte | 10 +++------- src/lib/components/PlanStopDialog.svelte | 9 +-------- src/lib/components/ShareSelector.svelte | 20 ++++++++----------- .../components/TextAppearanceSelector.svelte | 7 ++----- src/lib/data/stores/view.ts | 9 +++++++++ src/routes/+layout.svelte | 6 +----- 6 files changed, 24 insertions(+), 37 deletions(-) diff --git a/src/lib/components/DownloadSelector.svelte b/src/lib/components/DownloadSelector.svelte index 9cca68c34..d30dda4cf 100644 --- a/src/lib/components/DownloadSelector.svelte +++ b/src/lib/components/DownloadSelector.svelte @@ -4,7 +4,7 @@ A component for verse-on-image providing a dropdown where you can choose to down -->