|
58 | 58 | loader = defaultLoader, |
59 | 59 | audioId = $bindable(), |
60 | 60 | onchange = () => {}, |
| 61 | + readonly = false, |
61 | 62 | }: { |
62 | | - loader?: (audioId: string) => Promise<ReadableStream | undefined | typeof handled>, |
| 63 | + loader?: (audioId: string) => Promise<{stream: ReadableStream, filename: string} | undefined | typeof handled>, |
63 | 64 | audioId: string | undefined, |
64 | 65 | onchange?: (audioId: string | undefined) => void; |
| 66 | + readonly?: boolean; |
65 | 67 | } = $props(); |
66 | 68 |
|
67 | 69 | const projectContext = useProjectContext(); |
|
87 | 89 |
|
88 | 90 | return handled; |
89 | 91 | } |
90 | | - return await file.stream.stream(); |
| 92 | + return {stream: await file.stream.stream(), filename: file.fileName ?? ''}; |
91 | 93 | } |
92 | 94 |
|
93 | 95 | async function load() { |
94 | 96 | if (!audio || loadedAudioId === audioId || !audioId) return !!audioId; |
95 | 97 | playerState = 'loading'; |
96 | 98 | try { |
97 | | - const stream = await loader(audioId); |
98 | | - if (stream === handled) return false; |
99 | | - if (!stream) { |
| 99 | + const result = await loader(audioId); |
| 100 | + if (result === handled) return false; |
| 101 | + if (!result) { |
100 | 102 | AppNotification.error(`Failed to load audio ${audioId}`); |
101 | 103 | return; |
102 | 104 | } |
103 | | - let blob = await new Response(stream).blob(); |
| 105 | + let blob = await new Response(result.stream).blob(); |
104 | 106 | if (audio.src) URL.revokeObjectURL(audio.src); |
105 | 107 | loadedAudioId = undefined; |
106 | 108 | audio.src = URL.createObjectURL(blob); |
| 109 | + filename = result.filename; |
107 | 110 | loadedAudioId = audioId; |
108 | 111 | return true; |
109 | 112 | } finally { |
|
182 | 185 |
|
183 | 186 |
|
184 | 187 | let loadedAudioId = $state<string>(); |
| 188 | + let filename = $state(''); |
185 | 189 | let audio = $state<HTMLAudioElement>(); |
186 | 190 | let audioRuned = $derived(audio ? new AudioRuned(audio) : null); |
187 | 191 | useEventListener(() => audio, 'ended', () => playerState = 'paused'); |
|
233 | 237 | } |
234 | 238 | } |
235 | 239 |
|
| 240 | + async function onSaveAs() { |
| 241 | + if (!audio) return; |
| 242 | + await load(); |
| 243 | + //todo sadly this only works on desktop, not mobile, but it's the same with save as with the audio editor. |
| 244 | + const a = document.createElement('a'); |
| 245 | + a.href = audio.src; |
| 246 | + a.download = filename; |
| 247 | + a.click(); |
| 248 | + } |
| 249 | +
|
236 | 250 | function onAudioError(event: Event) { |
237 | 251 | if (audioHasKnownFlacSeekError()) { |
238 | 252 | console.log('Ignoring known FLAC seek error. Will try to recover on next play.'); |
|
253 | 267 | </script> |
254 | 268 | {#if supportsAudio} |
255 | 269 | {#if !audioId} |
256 | | - <Button variant="secondary" icon="i-mdi-microphone-plus" size="sm" iconProps={{class: 'size-5'}} onclick={onGetAudioClick}> |
257 | | - {$t`Add audio`} |
258 | | - </Button> |
| 270 | + {#if !readonly} |
| 271 | + <Button variant="secondary" icon="i-mdi-microphone-plus" size="sm" iconProps={{class: 'size-5'}} onclick={onGetAudioClick}> |
| 272 | + {$t`Add audio`} |
| 273 | + </Button> |
| 274 | + {:else} |
| 275 | + <div class="text-muted-foreground p-1"> |
| 276 | + {$t`No audio`} |
| 277 | + </div> |
| 278 | + {/if} |
259 | 279 | {:else if isNotFoundAudioId(audioId)} |
260 | 280 | <div class="text-muted-foreground p-1"> |
261 | 281 | {$t`Audio file not included in Send & Receive`} |
|
301 | 321 | {/snippet} |
302 | 322 | </ResponsiveMenu.Trigger> |
303 | 323 | <ResponsiveMenu.Content> |
304 | | - <ResponsiveMenu.Item icon="i-mdi-microphone-plus" onSelect={onGetAudioClick}> |
305 | | - {$t`Replace audio`} |
306 | | - </ResponsiveMenu.Item> |
307 | | - <ResponsiveMenu.Item icon="i-mdi-delete" onSelect={onRemoveAudio}> |
308 | | - {$t`Remove audio`} |
| 324 | + {#if !readonly} |
| 325 | + <ResponsiveMenu.Item icon="i-mdi-microphone-plus" onSelect={onGetAudioClick}> |
| 326 | + {$t`Replace audio`} |
| 327 | + </ResponsiveMenu.Item> |
| 328 | + <ResponsiveMenu.Item icon="i-mdi-delete" onSelect={onRemoveAudio}> |
| 329 | + {$t`Remove audio`} |
| 330 | + </ResponsiveMenu.Item> |
| 331 | + {/if} |
| 332 | + <ResponsiveMenu.Item icon="i-mdi-download" onSelect={onSaveAs}> |
| 333 | + {$t`Save As`} |
309 | 334 | </ResponsiveMenu.Item> |
310 | 335 | </ResponsiveMenu.Content> |
311 | 336 | </ResponsiveMenu.Root> |
|
0 commit comments