From 7deb7bd4d933b052a3f03e8b245b6f7778cdf91d Mon Sep 17 00:00:00 2001 From: b1rdmania <102524336+b1rdmania@users.noreply.github.com> Date: Fri, 26 Dec 2025 19:27:16 +0000 Subject: [PATCH] Move MIDI preview controls into results table. Adds per-row Play/Stop preview toggles, moves preview volume to the results header, and removes preview controls from the selected source card. --- index.html | 17 +++++--- src/main.ts | 114 ++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 96 insertions(+), 35 deletions(-) diff --git a/index.html b/index.html index b83d203..f43187e 100644 --- a/index.html +++ b/index.html @@ -413,6 +413,14 @@ } } + .results-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: calc(var(--spacing-unit) * 2); + flex-wrap: wrap; + margin: 0 0 calc(var(--spacing-unit) * 2) 0; + } .results-header h3 { margin: 0; } .confidence-bar { @@ -944,6 +952,10 @@

Search Results

+
+ + +
@@ -965,7 +977,6 @@
Search for a song, then choose a source to use.
-
@@ -976,10 +987,6 @@ -
- - -
Reimagined as classic game-console music using Motif’s Wario engine.
diff --git a/src/main.ts b/src/main.ts index cbd09b7..e6f3918 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,6 +11,9 @@ class MotifApp { // Preview player (lazily created for iOS compatibility) private soundfontPlayer: SoundfontMIDIPlayer | null = null; + private playingPreviewIndex: number | null = null; + private previewStopTimeout: number | null = null; + private previewButtons: HTMLButtonElement[] = []; private searchBtn!: HTMLButtonElement; private songInput!: HTMLInputElement; @@ -26,7 +29,6 @@ class MotifApp { // Preview player controls private soundfontVolumeSlider!: HTMLInputElement; - private previewBtn!: HTMLButtonElement; // Motif controls private motifBtn!: HTMLButtonElement; @@ -101,7 +103,6 @@ class MotifApp { // Preview player controls this.soundfontVolumeSlider = document.getElementById('soundfontVolume') as HTMLInputElement; - this.previewBtn = document.getElementById('previewBtn') as HTMLButtonElement; // Motif controls this.motifBtn = document.getElementById('motifBtn') as HTMLButtonElement; @@ -141,8 +142,7 @@ class MotifApp { } }); - // Preview MIDI (verification only) - this.previewBtn.addEventListener('click', () => void this.handlePreviewMIDI()); + // Preview MIDI (verification only) — controlled from results table rows this.soundfontVolumeSlider.addEventListener('input', (e) => { const volume = parseFloat((e.target as HTMLInputElement).value); this.soundfontPlayer?.setVolume(volume); @@ -295,6 +295,7 @@ class MotifApp { private displayResults(): void { this.resultsBody.innerHTML = ''; + this.previewButtons = []; this.searchResults.forEach((result, index) => { const row = document.createElement('tr'); @@ -302,9 +303,21 @@ class MotifApp { - + `; + const previewBtn = row.querySelector('button.row-preview-btn') as HTMLButtonElement; + previewBtn.addEventListener('click', (e) => { + e.stopPropagation(); + void this.handleRowPreview(index); + }); + this.previewButtons[index] = previewBtn; + // Use explicit action button to reduce accidental selection const useBtn = row.querySelector('button.row-use-btn') as HTMLButtonElement; useBtn.addEventListener('click', (e) => { @@ -322,6 +335,72 @@ class MotifApp { // No auto-select: user should choose "Use this" } + private updatePreviewButtons(): void { + for (let i = 0; i < this.previewButtons.length; i++) { + const btn = this.previewButtons[i]; + if (!btn) continue; + btn.textContent = this.playingPreviewIndex === i ? 'Stop' : 'Play'; + } + } + + private async handleRowPreview(index: number): Promise { + // Toggle stop if same row + if (this.playingPreviewIndex === index) { + this.stopPreview(true); + return; + } + + // Stop any existing preview first + this.stopPreview(false); + + const result = this.searchResults[index]; + if (!result?.midiUrl) return; + + try { + const player = await this.ensureAudioReady(); + + // Lazy-load events for preview without affecting selection state. + let events: NoteEvent[] | null = (result as any).__previewEvents || null; + if (!events) { + const midiBuffer = await this.midiService.fetchMIDI(result.midiUrl); + if (!midiBuffer) throw new Error('Failed to fetch MIDI'); + events = MIDIParser.parseMIDI(midiBuffer); + (result as any).__previewEvents = events; + } + + await player.load(events); + player.setVolume(parseFloat(this.soundfontVolumeSlider.value)); + await player.play(); + + this.playingPreviewIndex = index; + this.updatePreviewButtons(); + + // Best-effort: reset UI after playback ends (SoundfontMIDIPlayer self-stops) + const duration = player.getDuration(); + if (this.previewStopTimeout) window.clearTimeout(this.previewStopTimeout); + this.previewStopTimeout = window.setTimeout(() => { + if (this.playingPreviewIndex === index) { + this.stopPreview(false); + } + }, Math.max(0.5, duration + 0.5) * 1000); + } catch { + this.stopPreview(false); + } + } + + private stopPreview(updateStatus: boolean): void { + if (this.previewStopTimeout) { + window.clearTimeout(this.previewStopTimeout); + this.previewStopTimeout = null; + } + this.soundfontPlayer?.stop(); + this.playingPreviewIndex = null; + this.updatePreviewButtons(); + if (updateStatus) { + // No status spam in main flow. + } + } + public async selectResult(index: number): Promise { if (index < 0 || index >= this.searchResults.length) return; @@ -390,29 +469,6 @@ class MotifApp { } } - private async handlePreviewMIDI(): Promise { - if (!this.currentMIDI) return; - try { - this.previewBtn.disabled = true; - const prevLabel = this.previewBtn.textContent || 'Preview MIDI'; - this.previewBtn.textContent = 'Previewing…'; - - const player = await this.ensureAudioReady(); - await player.load(this.currentMIDI.events); - player.setVolume(parseFloat(this.soundfontVolumeSlider.value)); - await player.play(); - - const duration = player.getDuration(); - window.setTimeout(() => { - this.previewBtn.disabled = false; - this.previewBtn.textContent = prevLabel; - }, Math.max(0.5, duration + 0.25) * 1000); - } catch { - this.previewBtn.disabled = false; - this.previewBtn.textContent = 'Preview MIDI'; - } - } - // Motif handlers private async handleMotif(): Promise { if (!this.currentMIDI) { @@ -544,13 +600,11 @@ class MotifApp { private enablePlayerControls(): void { this.motifBtn.disabled = false; - this.previewBtn.disabled = false; this.copyLinkBtn.disabled = this.currentMIDI == null; } private disablePlayerControls(): void { this.motifBtn.disabled = true; - this.previewBtn.disabled = true; this.motifStopBtn.disabled = true; this.copyLinkBtn.disabled = true; }
${this.cleanSongTitle(result.title)} ${this.formatSourceLabel(result.source)} ${this.formatDuration(result.parsed?.durationSec)} +
+ + +
+