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.
This commit is contained in:
+12
-5
@@ -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 @@
|
||||
<div id="resultsSection" class="results-section">
|
||||
<div class="results-header">
|
||||
<h3>Search Results</h3>
|
||||
<div class="volume-control" style="margin-top:0; min-width: 260px; flex: 0 1 360px; opacity: 0.85;">
|
||||
<label for="soundfontVolume" style="font-size: 12px;">Preview volume</label>
|
||||
<input type="range" id="soundfontVolume" min="0" max="1" step="0.01" value="0.7" />
|
||||
</div>
|
||||
</div>
|
||||
<table id="resultsTable" class="results-table">
|
||||
<thead>
|
||||
@@ -965,7 +977,6 @@
|
||||
<div id="selectedMeta" class="selected-meta">Search for a song, then choose a source to use.</div>
|
||||
|
||||
<div class="selected-actions">
|
||||
<button id="previewBtn" type="button" disabled>Preview MIDI</button>
|
||||
<button id="motifBtn" class="primary" disabled>Generate</button>
|
||||
</div>
|
||||
|
||||
@@ -976,10 +987,6 @@
|
||||
<label for="motifVolume">Output</label>
|
||||
<input type="range" id="motifVolume" min="0" max="1" step="0.01" value="0.7" />
|
||||
</div>
|
||||
<div class="volume-control">
|
||||
<label for="soundfontVolume">Preview</label>
|
||||
<input type="range" id="soundfontVolume" min="0" max="1" step="0.01" value="0.7" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="engine-line">Reimagined as classic game-console music using Motif’s Wario engine.</div>
|
||||
|
||||
+84
-30
@@ -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 {
|
||||
<td>${this.cleanSongTitle(result.title)}</td>
|
||||
<td class="source-col">${this.formatSourceLabel(result.source)}</td>
|
||||
<td class="duration-col">${this.formatDuration(result.parsed?.durationSec)}</td>
|
||||
<td class="action-col"><button type="button" class="row-use-btn">Use this</button></td>
|
||||
<td class="action-col">
|
||||
<div style="display:flex; gap: 10px; justify-content: flex-end; align-items:center;">
|
||||
<button type="button" class="row-preview-btn">Play</button>
|
||||
<button type="button" class="row-use-btn">Use this</button>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
|
||||
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<void> {
|
||||
// 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<void> {
|
||||
if (index < 0 || index >= this.searchResults.length) return;
|
||||
|
||||
@@ -390,29 +469,6 @@ class MotifApp {
|
||||
}
|
||||
}
|
||||
|
||||
private async handlePreviewMIDI(): Promise<void> {
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user