From bbaee43c74b9e0f104cce239c51fcf9b128f2470 Mon Sep 17 00:00:00 2001 From: b1rdmania <102524336+b1rdmania@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:23:36 +0000 Subject: [PATCH] fix: make mp3 export resilient to async gesture loss --- index.html | 1 - src/lamejs.d.ts | 1 - src/main.ts | 106 ++++++++++++++++++++++++++++++++++-------------- 3 files changed, 75 insertions(+), 33 deletions(-) diff --git a/index.html b/index.html index 06b7a9a..174ab6d 100644 --- a/index.html +++ b/index.html @@ -1300,7 +1300,6 @@ - diff --git a/src/lamejs.d.ts b/src/lamejs.d.ts index 849b84d..f022cc2 100644 --- a/src/lamejs.d.ts +++ b/src/lamejs.d.ts @@ -5,4 +5,3 @@ declare module 'lamejs' { const _default: any; export default _default; } - diff --git a/src/main.ts b/src/main.ts index 076543e..0bfc992 100644 --- a/src/main.ts +++ b/src/main.ts @@ -833,35 +833,50 @@ class MotifApp { return out; } - private downloadBlob(blob: Blob, filename: string): void { - const url = URL.createObjectURL(blob); - try { - const a = document.createElement('a'); - a.href = url; - a.download = filename; - a.rel = 'noopener'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - } finally { - window.setTimeout(() => { - try { - URL.revokeObjectURL(url); - } catch { - // ignore - } - }, 2000); - } - } - private async handleDownloadMp3(): Promise { if (!this.hasGenerated || !this.currentMIDI) return; + const result = this.searchResults[this.selectedResultIndex]; + const safeTitle = this.cleanSongTitle(result?.title || 'wario-synth').slice(0, 80) || 'wario-synth'; + const fileName = `${safeTitle}.mp3`; + let fileHandle: any = null; + let popup: Window | null = null; + try { this.downloadMp3Btn.disabled = true; this.downloadMp3Btn.textContent = 'Rendering…'; this.updateStatus('Rendering audio…'); + // Capture user activation up-front when possible. + const maybeShowSaveFilePicker = (window as any).showSaveFilePicker; + if (typeof maybeShowSaveFilePicker === 'function') { + try { + fileHandle = await maybeShowSaveFilePicker({ + suggestedName: fileName, + types: [{ + description: 'MP3 audio', + accept: { 'audio/mpeg': ['.mp3'] }, + }], + }); + } catch (e: any) { + if (e?.name === 'AbortError') { + this.updateStatus('Download cancelled.'); + return; + } + } + } else { + // Fallback for browsers without file picker: pre-open a window from the click gesture. + popup = window.open('', '_blank'); + if (popup && !popup.closed) { + try { + popup.document.title = 'Preparing MP3'; + popup.document.body.innerHTML = '

Preparing MP3…

'; + } catch { + // ignore cross-window access issues + } + } + } + // Stop any current playback this.handleMotifStop(); @@ -878,10 +893,9 @@ class MotifApp { const Mp3Encoder = (lamejs as any)?.Mp3Encoder; if (!Mp3Encoder) throw new Error('MP3 encoder not loaded'); - const channels = audioBuffer.numberOfChannels; const kbps = 128; - const encoder = new Mp3Encoder(channels, sampleRate, kbps); + const encoder = new (lamejs as any).Mp3Encoder(channels, sampleRate, kbps); const mp3Chunks: BlobPart[] = []; // Get audio data @@ -903,15 +917,45 @@ class MotifApp { if (tail && tail.length) mp3Chunks.push(tail); const blob = new Blob(mp3Chunks, { type: 'audio/mpeg' }); - const result = this.searchResults[this.selectedResultIndex]; - const safeTitle = this.cleanSongTitle(result?.title || 'wario-synth').slice(0, 80) || 'wario-synth'; - const filename = `${safeTitle}.mp3`; + if (fileHandle) { + const writable = await fileHandle.createWritable(); + await writable.write(blob); + await writable.close(); + this.updateStatus('MP3 downloaded!'); + setTimeout(() => this.updateStatus(''), 1500); + return; + } - this.downloadMp3Btn.textContent = 'Downloading…'; - this.updateStatus('Downloading…'); - this.downloadBlob(blob, filename); + const url = URL.createObjectURL(blob); + let attemptedDownload = false; + try { + const a = document.createElement('a'); + a.href = url; + a.download = fileName; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + attemptedDownload = true; + } catch { + // fallback below + } - window.setTimeout(() => this.updateStatus(''), 1200); + if (popup && !popup.closed) { + popup.location.href = url; + if (attemptedDownload) { + popup.close(); + } + } else if (!attemptedDownload) { + window.open(url, '_blank'); + } + + if (attemptedDownload) { + this.updateStatus('MP3 downloaded!'); + setTimeout(() => this.updateStatus(''), 1500); + } else { + this.updateStatus('MP3 opened in a new tab. Save it from there.'); + } + setTimeout(() => URL.revokeObjectURL(url), 60_000); } catch (e) { this.updateStatus(`MP3 failed: ${e instanceof Error ? e.message : 'Unknown error'}`); } finally { @@ -1062,4 +1106,4 @@ try { status.textContent = `App error: ${err instanceof Error ? err.message : 'Unknown error'}. Please refresh.`; status.style.color = '#ff6b6b'; } -} \ No newline at end of file +}