Split selected vs generated UI on main page.

After generation starts, hide preview/source-changing/search affordances and switch to an artifact view: title, Wario transformation line, Play/Pause, timeline, volume, and Copy link.
This commit is contained in:
b1rdmania
2025-12-26 21:14:30 +00:00
parent fd422aa084
commit 63618ec4fa
2 changed files with 147 additions and 30 deletions
+34 -15
View File
@@ -1026,17 +1026,17 @@
<div id="selectedCard" class="selected-card">
<h2 id="selectedTitle" class="selected-title">No source selected</h2>
<div id="selectedMeta" class="selected-meta">Search for a song, then choose a source to use.</div>
<!-- selected_pre_generate only -->
<div id="selectedExpectation" class="selected-meta" style="margin-top: 8px; color: rgba(255,255,255,0.62);">
Some MIDI files work better than others. If the result sounds off, try another source.
</div>
<div class="selected-actions">
<div class="selected-actions" id="preGenActions">
<button id="motifBtn" class="primary" disabled>Generate music</button>
</div>
<div class="selected-support">
<button id="motifStopBtn" type="button" disabled>Stop</button>
<button id="copyLinkBtn" type="button" disabled style="display:none;">Copy link</button>
<div class="selected-support" id="preGenSupport">
<div class="preview-inline">
<button id="previewBtn" type="button" class="linklike" disabled>Preview original MIDI</button>
<span id="previewState" class="preview-state">Previewing…</span>
@@ -1045,30 +1045,49 @@
<button id="chooseDifferentBtn" type="button" class="linklike">Choose a different source</button>
</div>
<div class="engine-line">Reimagined as classic game-console music using Motifs Wario engine.</div>
<div id="iosAudioBanner" class="ios-audio-banner" aria-live="polite">
<div class="row">
<div class="copy">
<strong>iOS Safari:</strong> tap once to enable audio, then press <strong>Generate</strong>.
<strong>iOS Safari:</strong> tap once to enable audio, then press <strong>Generate music</strong>.
</div>
<button id="enableAudioBtn" type="button">Enable Audio</button>
</div>
<div id="iosAudioState" class="state">Audio: …</div>
</div>
<div class="motif-progress-container" id="motifProgressContainer" style="display: none;">
<div class="progress-time">
<span id="motifCurrentTime">0:00</span>
<span id="motifDuration">0:00</span>
<!-- generated only: artifact playback + sharing -->
<div id="generatedBlock" style="display:none;">
<div class="engine-line" id="generatedTransform">
Reimagined as classic game-console music<br />
using Motifs Wario engine.
</div>
<div class="progress-bar-wrapper">
<input type="range" id="motifProgressBar" class="progress-bar" min="0" max="100" value="0" step="0.1" />
<div class="progress-fill" id="motifProgressFill"></div>
<div style="display:flex; justify-content:center; margin-top: 14px; margin-bottom: 14px;">
<button id="playPauseBtn" class="primary" type="button">Play</button>
</div>
<div class="motif-progress-container" id="motifProgressContainer" style="display: none;">
<div class="progress-time">
<span id="motifCurrentTime">0:00</span>
<span id="motifDuration">0:00</span>
</div>
<div class="progress-bar-wrapper">
<input type="range" id="motifProgressBar" class="progress-bar" min="0" max="100" value="0" step="0.1" />
<div class="progress-fill" id="motifProgressFill"></div>
</div>
</div>
<div class="volume-control" id="generatedVolumeRow" style="margin-top: 14px; opacity: 0.75;">
<label for="motifVolume" style="min-width: auto;">Volume</label>
<input type="range" id="motifVolume" min="0" max="1" step="0.01" value="0.8" />
</div>
<div style="margin-top: 14px;">
<button id="copyLinkBtn" type="button" disabled>Copy link</button>
</div>
</div>
<details class="details-toggle">
<details class="details-toggle" id="detailsToggle">
<summary>Details</summary>
<div id="selectedDetails" class="selected-details"></div>
</details>
+113 -15
View File
@@ -16,6 +16,7 @@ class MotifApp {
private searchBtn!: HTMLButtonElement;
private songInput!: HTMLInputElement;
private status!: HTMLElement;
private controlsEl!: HTMLElement;
private resultsSection!: HTMLElement;
private resultsBody!: HTMLElement;
@@ -25,6 +26,13 @@ class MotifApp {
private selectedTitle!: HTMLElement;
private selectedMeta!: HTMLElement;
private selectedDetails!: HTMLElement;
private selectedExpectation!: HTMLElement;
private preGenActions!: HTMLElement;
private preGenSupport!: HTMLElement;
private generatedBlock!: HTMLElement;
private playPauseBtn!: HTMLButtonElement;
private motifVolumeSlider!: HTMLInputElement;
private detailsToggle!: HTMLElement;
private previewBtn!: HTMLButtonElement;
private previewStopBtn!: HTMLButtonElement;
private previewState!: HTMLElement;
@@ -32,12 +40,13 @@ class MotifApp {
private isPreviewPlaying = false;
private hasGenerated = false;
private isMotifPlaying = false;
private motifResumeProgress = 0;
// Preview player (no UI volume)
// Motif controls
private motifBtn!: HTMLButtonElement;
private motifStopBtn!: HTMLButtonElement;
private motifProgressContainer!: HTMLElement;
private motifProgressBar!: HTMLInputElement;
private motifProgressFill!: HTMLElement;
@@ -97,6 +106,7 @@ class MotifApp {
this.searchBtn = document.getElementById('searchBtn') as HTMLButtonElement;
this.songInput = document.getElementById('songInput') as HTMLInputElement;
this.status = document.getElementById('status')!;
this.controlsEl = document.querySelector('.controls') as HTMLElement;
this.resultsSection = document.getElementById('resultsSection')!;
this.resultsBody = document.getElementById('resultsBody')!;
@@ -106,6 +116,13 @@ class MotifApp {
this.selectedTitle = document.getElementById('selectedTitle')!;
this.selectedMeta = document.getElementById('selectedMeta')!;
this.selectedDetails = document.getElementById('selectedDetails')!;
this.selectedExpectation = document.getElementById('selectedExpectation')!;
this.preGenActions = document.getElementById('preGenActions')!;
this.preGenSupport = document.getElementById('preGenSupport')!;
this.generatedBlock = document.getElementById('generatedBlock')!;
this.playPauseBtn = document.getElementById('playPauseBtn') as HTMLButtonElement;
this.motifVolumeSlider = document.getElementById('motifVolume') as HTMLInputElement;
this.detailsToggle = document.getElementById('detailsToggle')!;
this.previewBtn = document.getElementById('previewBtn') as HTMLButtonElement;
this.previewStopBtn = document.getElementById('previewStopBtn') as HTMLButtonElement;
this.previewState = document.getElementById('previewState')!;
@@ -113,7 +130,6 @@ class MotifApp {
// Motif controls
this.motifBtn = document.getElementById('motifBtn') as HTMLButtonElement;
this.motifStopBtn = document.getElementById('motifStopBtn') as HTMLButtonElement;
this.motifProgressContainer = document.getElementById('motifProgressContainer')!;
this.motifProgressBar = document.getElementById('motifProgressBar') as HTMLInputElement;
this.motifProgressFill = document.getElementById('motifProgressFill')!;
@@ -153,7 +169,10 @@ class MotifApp {
// Motif
this.motifBtn.addEventListener('click', () => this.handleMotif());
this.motifStopBtn.addEventListener('click', () => this.handleMotifStop());
this.playPauseBtn.addEventListener('click', () => void this.handlePlayPause());
this.motifVolumeSlider.addEventListener('input', () => {
this.motifEngine.setVolume(parseFloat(this.motifVolumeSlider.value));
});
// Preview inside selected source only
this.previewBtn.addEventListener('click', () => void this.handlePreviewToggle());
@@ -433,7 +452,8 @@ class MotifApp {
this.previewBtn.disabled = true;
const player = await this.ensureAudioReady();
await player.load(this.currentMIDI.events);
player.setVolume(1);
// conservative fixed preview level
player.setVolume(0.8);
await player.play();
this.isPreviewPlaying = true;
@@ -464,16 +484,21 @@ class MotifApp {
await unlockAudio();
this.updateIOSAudioBanner();
// Once generation starts: lock into generated experience mode.
this.setState('generated');
this.updateStatus('Generating…');
this.motifBtn.disabled = true;
this.playPauseBtn.disabled = true;
// Generate a variation using the procedural role-mapping mode
await this.motifEngine.generateFromMIDI(this.currentMIDI.events, 'procedural');
this.motifEngine.setVolume(1);
this.motifEngine.setVolume(parseFloat(this.motifVolumeSlider.value) || 0.8);
await this.motifEngine.play();
this.motifStopBtn.disabled = false;
this.isMotifPlaying = true;
this.playPauseBtn.disabled = false;
this.playPauseBtn.textContent = 'Pause';
// Show progress bar and set duration
this.motifProgressContainer.style.display = 'block';
@@ -487,24 +512,35 @@ class MotifApp {
this.updateStatus('');
this.hasGenerated = true;
this.setState('generated');
// keep generated state
} catch (error) {
this.updateStatus(`Motif error: ${error instanceof Error ? error.message : 'Unknown error'}`);
this.motifBtn.disabled = false;
this.playPauseBtn.disabled = false;
// If generation fails, return to selected state
this.setState('selected');
}
}
private handleMotifStop(): void {
this.motifEngine.stop();
this.motifBtn.disabled = false;
this.motifStopBtn.disabled = true;
this.isMotifPlaying = false;
this.playPauseBtn.textContent = 'Play';
this.stopMotifProgressUpdates();
this.updateStatus('');
}
private handleMotifSeek(progress: number): void {
this.motifEngine.seek(progress);
this.updateMotifProgress();
this.motifResumeProgress = progress;
if (this.isMotifPlaying) {
this.motifEngine.seek(progress);
this.updateMotifProgress();
return;
}
const duration = this.motifEngine.getDuration();
this.motifProgressBar.value = (progress * 100).toString();
this.motifProgressFill.style.width = `${progress * 100}%`;
this.motifCurrentTime.textContent = this.formatTime(progress * duration);
}
private startMotifProgressUpdates(): void {
@@ -528,6 +564,16 @@ class MotifApp {
this.motifProgressBar.value = (progress * 100).toString();
this.motifProgressFill.style.width = `${progress * 100}%`;
this.motifCurrentTime.textContent = this.formatTime(currentTime);
// Auto-pause at end
const duration = this.motifEngine.getDuration();
if (duration > 0 && progress >= 0.999) {
this.motifResumeProgress = 0;
this.handleMotifStop();
this.motifCurrentTime.textContent = this.formatTime(duration);
this.motifProgressBar.value = '100';
this.motifProgressFill.style.width = '100%';
}
}
private formatTime(seconds: number): string {
@@ -588,7 +634,6 @@ class MotifApp {
private disablePlayerControls(): void {
this.motifBtn.disabled = true;
this.motifStopBtn.disabled = true;
this.copyLinkBtn.disabled = true;
this.previewBtn.disabled = true;
}
@@ -683,7 +728,7 @@ class MotifApp {
private setState(state: 'idle' | 'results' | 'selected' | 'generated'): void {
// results
const hasResults = state === 'results' || state === 'selected' || state === 'generated';
const hasResults = state === 'results' || state === 'selected';
this.resultsSection.classList.toggle('visible', hasResults);
this.chooseHelper.style.display = state === 'results' ? 'block' : 'none';
@@ -693,10 +738,35 @@ class MotifApp {
// share link only after generation
this.copyLinkBtn.style.display = state === 'generated' ? 'inline-block' : 'none';
this.copyLinkBtn.disabled = state !== 'generated';
this.copyLinkBtn.disabled = !(state === 'generated' && this.hasGenerated);
// collapse results when selected/generated
this.resultsSection.classList.toggle('collapsed', state === 'selected' || state === 'generated');
this.resultsSection.classList.toggle('collapsed', state === 'selected');
// Search affordances: hidden after generation (no choice)
const hideSearch = state === 'generated';
this.controlsEl.style.display = hideSearch ? 'none' : '';
this.status.style.display = hideSearch ? 'none' : '';
// Selected pre-generation content
const showPreGen = state === 'selected';
this.selectedExpectation.style.display = showPreGen ? 'block' : 'none';
this.preGenActions.style.display = showPreGen ? 'block' : 'none';
this.preGenSupport.style.display = showPreGen ? 'flex' : 'none';
this.detailsToggle.style.display = showPreGen ? 'block' : 'none';
// Generated artifact content
const showGenerated = state === 'generated';
this.generatedBlock.style.display = showGenerated ? 'block' : 'none';
this.playPauseBtn.disabled = !showGenerated || !this.hasGenerated;
this.playPauseBtn.textContent = this.isMotifPlaying ? 'Pause' : 'Play';
// iOS banner only before generation
if (showPreGen) {
this.updateIOSAudioBanner();
} else {
this.iosAudioBanner.style.display = 'none';
}
}
private goToResults(): void {
@@ -708,6 +778,34 @@ class MotifApp {
}
}
private async handlePlayPause(): Promise<void> {
if (!this.hasGenerated) return;
if (this.isMotifPlaying) {
this.motifResumeProgress = this.motifEngine.getProgress();
this.motifEngine.stop();
this.isMotifPlaying = false;
this.stopMotifProgressUpdates();
this.playPauseBtn.textContent = 'Play';
return;
}
try {
// Audio exclusivity
this.stopPreview();
await unlockAudio();
this.motifEngine.setVolume(parseFloat(this.motifVolumeSlider.value) || 0.8);
await this.motifEngine.play();
if (this.motifResumeProgress > 0) this.motifEngine.seek(this.motifResumeProgress);
this.isMotifPlaying = true;
this.playPauseBtn.textContent = 'Pause';
this.startMotifProgressUpdates();
} catch {
// ignore
}
}
private updateEmbedSnippet(songTitle: string): void {
if (!this.embedSection || !this.embedCodeEl) return;