Make results a chooser and move preview to selected card.
Removes preview controls from search results, adds a 'Choosing a MIDI source' helper block, introduces selected/generated states, and keeps Preview original MIDI as a secondary action inside the selected source card.
This commit is contained in:
+64
-7
@@ -333,6 +333,29 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chooser-helper {
|
||||||
|
margin-top: calc(var(--spacing-unit) * 2);
|
||||||
|
padding: calc(var(--spacing-unit) * 2);
|
||||||
|
background: rgba(0,0,0,0.18);
|
||||||
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
.chooser-helper h4 {
|
||||||
|
margin: 0 0 6px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgba(255,255,255,0.85);
|
||||||
|
}
|
||||||
|
.chooser-helper p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255,255,255,0.68);
|
||||||
|
line-height: 1.45;
|
||||||
|
max-width: 72ch;
|
||||||
|
}
|
||||||
|
|
||||||
.results-table {
|
.results-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
@@ -486,11 +509,28 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
.selected-support .volume-control {
|
.selected-support .linklike {
|
||||||
margin-top: 0;
|
background: transparent;
|
||||||
flex: 1;
|
border: none;
|
||||||
min-width: 220px;
|
padding: 0;
|
||||||
opacity: 0.85;
|
box-shadow: none;
|
||||||
|
color: rgba(0,255,136,0.9);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.selected-support .linklike:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.preview-inline {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.preview-state {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255,255,255,0.55);
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
.engine-line {
|
.engine-line {
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
@@ -957,6 +997,14 @@
|
|||||||
<div id="status">Ready. Enter a song name to search for MIDI files.</div>
|
<div id="status">Ready. Enter a song name to search for MIDI files.</div>
|
||||||
|
|
||||||
<div id="resultsSection" class="results-section">
|
<div id="resultsSection" class="results-section">
|
||||||
|
<div id="chooseHelper" class="chooser-helper" style="display:none;">
|
||||||
|
<h4>Choosing a MIDI source</h4>
|
||||||
|
<p>
|
||||||
|
Different MIDI files capture songs differently. Some will produce better results than others.<br />
|
||||||
|
Pick a source, generate, and if it sounds off, try another.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="results-header">
|
<div class="results-header">
|
||||||
<h3>Search Results</h3>
|
<h3>Search Results</h3>
|
||||||
</div>
|
</div>
|
||||||
@@ -978,14 +1026,23 @@
|
|||||||
<div id="selectedCard" class="selected-card">
|
<div id="selectedCard" class="selected-card">
|
||||||
<h2 id="selectedTitle" class="selected-title">No source selected</h2>
|
<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>
|
<div id="selectedMeta" class="selected-meta">Search for a song, then choose a source to use.</div>
|
||||||
|
<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">
|
||||||
<button id="motifBtn" class="primary" disabled>Generate</button>
|
<button id="motifBtn" class="primary" disabled>Generate music</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="selected-support">
|
<div class="selected-support">
|
||||||
<button id="motifStopBtn" type="button" disabled>Stop</button>
|
<button id="motifStopBtn" type="button" disabled>Stop</button>
|
||||||
<button id="copyLinkBtn" type="button" disabled>Copy link</button>
|
<button id="copyLinkBtn" type="button" disabled style="display:none;">Copy link</button>
|
||||||
|
<div class="preview-inline">
|
||||||
|
<button id="previewBtn" type="button" class="linklike" disabled>Preview original MIDI</button>
|
||||||
|
<span id="previewState" class="preview-state">Previewing…</span>
|
||||||
|
<button id="previewStopBtn" type="button" class="linklike" style="display:none;">Stop preview</button>
|
||||||
|
</div>
|
||||||
|
<button id="chooseDifferentBtn" type="button" class="linklike">Choose a different source</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="engine-line">Reimagined as classic game-console music using Motif’s Wario engine.</div>
|
<div class="engine-line">Reimagined as classic game-console music using Motif’s Wario engine.</div>
|
||||||
|
|||||||
+95
-81
@@ -11,9 +11,7 @@ class MotifApp {
|
|||||||
|
|
||||||
// Preview player (lazily created for iOS compatibility)
|
// Preview player (lazily created for iOS compatibility)
|
||||||
private soundfontPlayer: SoundfontMIDIPlayer | null = null;
|
private soundfontPlayer: SoundfontMIDIPlayer | null = null;
|
||||||
private playingPreviewIndex: number | null = null;
|
|
||||||
private previewStopTimeout: number | null = null;
|
private previewStopTimeout: number | null = null;
|
||||||
private previewButtons: HTMLButtonElement[] = [];
|
|
||||||
|
|
||||||
private searchBtn!: HTMLButtonElement;
|
private searchBtn!: HTMLButtonElement;
|
||||||
private songInput!: HTMLInputElement;
|
private songInput!: HTMLInputElement;
|
||||||
@@ -22,10 +20,18 @@ class MotifApp {
|
|||||||
private resultsSection!: HTMLElement;
|
private resultsSection!: HTMLElement;
|
||||||
private resultsBody!: HTMLElement;
|
private resultsBody!: HTMLElement;
|
||||||
private playerSection!: HTMLElement;
|
private playerSection!: HTMLElement;
|
||||||
|
private chooseHelper!: HTMLElement;
|
||||||
|
|
||||||
private selectedTitle!: HTMLElement;
|
private selectedTitle!: HTMLElement;
|
||||||
private selectedMeta!: HTMLElement;
|
private selectedMeta!: HTMLElement;
|
||||||
private selectedDetails!: HTMLElement;
|
private selectedDetails!: HTMLElement;
|
||||||
|
private previewBtn!: HTMLButtonElement;
|
||||||
|
private previewStopBtn!: HTMLButtonElement;
|
||||||
|
private previewState!: HTMLElement;
|
||||||
|
private chooseDifferentBtn!: HTMLButtonElement;
|
||||||
|
|
||||||
|
private isPreviewPlaying = false;
|
||||||
|
private hasGenerated = false;
|
||||||
|
|
||||||
// Preview player (no UI volume)
|
// Preview player (no UI volume)
|
||||||
|
|
||||||
@@ -95,10 +101,15 @@ class MotifApp {
|
|||||||
this.resultsSection = document.getElementById('resultsSection')!;
|
this.resultsSection = document.getElementById('resultsSection')!;
|
||||||
this.resultsBody = document.getElementById('resultsBody')!;
|
this.resultsBody = document.getElementById('resultsBody')!;
|
||||||
this.playerSection = document.getElementById('playerSection')!;
|
this.playerSection = document.getElementById('playerSection')!;
|
||||||
|
this.chooseHelper = document.getElementById('chooseHelper')!;
|
||||||
|
|
||||||
this.selectedTitle = document.getElementById('selectedTitle')!;
|
this.selectedTitle = document.getElementById('selectedTitle')!;
|
||||||
this.selectedMeta = document.getElementById('selectedMeta')!;
|
this.selectedMeta = document.getElementById('selectedMeta')!;
|
||||||
this.selectedDetails = document.getElementById('selectedDetails')!;
|
this.selectedDetails = document.getElementById('selectedDetails')!;
|
||||||
|
this.previewBtn = document.getElementById('previewBtn') as HTMLButtonElement;
|
||||||
|
this.previewStopBtn = document.getElementById('previewStopBtn') as HTMLButtonElement;
|
||||||
|
this.previewState = document.getElementById('previewState')!;
|
||||||
|
this.chooseDifferentBtn = document.getElementById('chooseDifferentBtn') as HTMLButtonElement;
|
||||||
|
|
||||||
// Motif controls
|
// Motif controls
|
||||||
this.motifBtn = document.getElementById('motifBtn') as HTMLButtonElement;
|
this.motifBtn = document.getElementById('motifBtn') as HTMLButtonElement;
|
||||||
@@ -143,6 +154,11 @@ class MotifApp {
|
|||||||
// Motif
|
// Motif
|
||||||
this.motifBtn.addEventListener('click', () => this.handleMotif());
|
this.motifBtn.addEventListener('click', () => this.handleMotif());
|
||||||
this.motifStopBtn.addEventListener('click', () => this.handleMotifStop());
|
this.motifStopBtn.addEventListener('click', () => this.handleMotifStop());
|
||||||
|
|
||||||
|
// Preview inside selected source only
|
||||||
|
this.previewBtn.addEventListener('click', () => void this.handlePreviewToggle());
|
||||||
|
this.previewStopBtn.addEventListener('click', () => this.stopPreview());
|
||||||
|
this.chooseDifferentBtn.addEventListener('click', () => this.goToResults());
|
||||||
// Use both input and change for iOS compatibility
|
// Use both input and change for iOS compatibility
|
||||||
const seekHandler = (e: Event) => {
|
const seekHandler = (e: Event) => {
|
||||||
const progress = parseFloat((e.target as HTMLInputElement).value) / 100;
|
const progress = parseFloat((e.target as HTMLInputElement).value) / 100;
|
||||||
@@ -249,7 +265,7 @@ class MotifApp {
|
|||||||
|
|
||||||
this.updateStatus('Searching…');
|
this.updateStatus('Searching…');
|
||||||
this.searchBtn.disabled = true;
|
this.searchBtn.disabled = true;
|
||||||
this.hideResults();
|
this.setState('idle');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const results = await this.midiService.search(songName);
|
const results = await this.midiService.search(songName);
|
||||||
@@ -272,6 +288,7 @@ class MotifApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.displayResults();
|
this.displayResults();
|
||||||
|
this.setState('results');
|
||||||
this.updateStatus('Pick a source to use.');
|
this.updateStatus('Pick a source to use.');
|
||||||
this.updateIOSAudioBanner();
|
this.updateIOSAudioBanner();
|
||||||
|
|
||||||
@@ -284,7 +301,6 @@ class MotifApp {
|
|||||||
|
|
||||||
private displayResults(): void {
|
private displayResults(): void {
|
||||||
this.resultsBody.innerHTML = '';
|
this.resultsBody.innerHTML = '';
|
||||||
this.previewButtons = [];
|
|
||||||
|
|
||||||
this.searchResults.forEach((result, index) => {
|
this.searchResults.forEach((result, index) => {
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
@@ -294,19 +310,11 @@ class MotifApp {
|
|||||||
<td class="duration-col">${this.formatDuration(result.parsed?.durationSec)}</td>
|
<td class="duration-col">${this.formatDuration(result.parsed?.durationSec)}</td>
|
||||||
<td class="action-col">
|
<td class="action-col">
|
||||||
<div style="display:flex; gap: 10px; justify-content: flex-end; align-items:center;">
|
<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>
|
||||||
<button type="button" class="row-use-btn">Use this</button>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</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
|
// Use explicit action button to reduce accidental selection
|
||||||
const useBtn = row.querySelector('button.row-use-btn') as HTMLButtonElement;
|
const useBtn = row.querySelector('button.row-use-btn') as HTMLButtonElement;
|
||||||
useBtn.addEventListener('click', (e) => {
|
useBtn.addEventListener('click', (e) => {
|
||||||
@@ -317,77 +325,19 @@ class MotifApp {
|
|||||||
this.resultsBody.appendChild(row);
|
this.resultsBody.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.resultsSection.classList.add('visible');
|
|
||||||
this.resultsSection.classList.remove('collapsed');
|
|
||||||
this.playerSection.classList.remove('visible');
|
|
||||||
|
|
||||||
// No auto-select: user should choose "Use this"
|
// No auto-select: user should choose "Use this"
|
||||||
}
|
}
|
||||||
|
|
||||||
private updatePreviewButtons(): void {
|
private stopPreview(): 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(1);
|
|
||||||
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) {
|
if (this.previewStopTimeout) {
|
||||||
window.clearTimeout(this.previewStopTimeout);
|
window.clearTimeout(this.previewStopTimeout);
|
||||||
this.previewStopTimeout = null;
|
this.previewStopTimeout = null;
|
||||||
}
|
}
|
||||||
this.soundfontPlayer?.stop();
|
this.soundfontPlayer?.stop();
|
||||||
this.playingPreviewIndex = null;
|
this.isPreviewPlaying = false;
|
||||||
this.updatePreviewButtons();
|
this.previewState.style.display = 'none';
|
||||||
if (updateStatus) {
|
this.previewStopBtn.style.display = 'none';
|
||||||
// No status spam in main flow.
|
this.previewBtn.disabled = this.currentMIDI == null;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async selectResult(index: number): Promise<void> {
|
public async selectResult(index: number): Promise<void> {
|
||||||
@@ -397,6 +347,8 @@ class MotifApp {
|
|||||||
this.handleMotifStop();
|
this.handleMotifStop();
|
||||||
// Stop any playing preview audio
|
// Stop any playing preview audio
|
||||||
this.soundfontPlayer?.stop();
|
this.soundfontPlayer?.stop();
|
||||||
|
this.stopPreview();
|
||||||
|
this.hasGenerated = false;
|
||||||
|
|
||||||
this.selectedResultIndex = index;
|
this.selectedResultIndex = index;
|
||||||
const result = this.searchResults[index];
|
const result = this.searchResults[index];
|
||||||
@@ -452,6 +404,7 @@ class MotifApp {
|
|||||||
this.resultsSection.classList.add('collapsed');
|
this.resultsSection.classList.add('collapsed');
|
||||||
this.enablePlayerControls();
|
this.enablePlayerControls();
|
||||||
this.updateStatus('');
|
this.updateStatus('');
|
||||||
|
this.setState('selected');
|
||||||
|
|
||||||
// Intent: after selecting "Use this", bring the workbench controls into view.
|
// Intent: after selecting "Use this", bring the workbench controls into view.
|
||||||
// Do this after the card is visible and MIDI is loaded.
|
// Do this after the card is visible and MIDI is loaded.
|
||||||
@@ -466,6 +419,38 @@ class MotifApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async handlePreviewToggle(): Promise<void> {
|
||||||
|
if (!this.currentMIDI) return;
|
||||||
|
if (this.isPreviewPlaying) {
|
||||||
|
this.stopPreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio exclusivity
|
||||||
|
this.handleMotifStop();
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.previewBtn.disabled = true;
|
||||||
|
const player = await this.ensureAudioReady();
|
||||||
|
await player.load(this.currentMIDI.events);
|
||||||
|
player.setVolume(1);
|
||||||
|
await player.play();
|
||||||
|
|
||||||
|
this.isPreviewPlaying = true;
|
||||||
|
this.previewState.style.display = 'inline';
|
||||||
|
this.previewStopBtn.style.display = 'inline';
|
||||||
|
this.previewBtn.disabled = false;
|
||||||
|
|
||||||
|
const duration = player.getDuration();
|
||||||
|
if (this.previewStopTimeout) window.clearTimeout(this.previewStopTimeout);
|
||||||
|
this.previewStopTimeout = window.setTimeout(() => {
|
||||||
|
this.stopPreview();
|
||||||
|
}, Math.max(0.5, duration + 0.5) * 1000);
|
||||||
|
} catch {
|
||||||
|
this.stopPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Motif handlers
|
// Motif handlers
|
||||||
private async handleMotif(): Promise<void> {
|
private async handleMotif(): Promise<void> {
|
||||||
if (!this.currentMIDI) {
|
if (!this.currentMIDI) {
|
||||||
@@ -473,6 +458,8 @@ class MotifApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Audio exclusivity
|
||||||
|
this.stopPreview();
|
||||||
// Best-effort: ensure iOS audio is unlocked from this user gesture.
|
// Best-effort: ensure iOS audio is unlocked from this user gesture.
|
||||||
await unlockAudio();
|
await unlockAudio();
|
||||||
this.updateIOSAudioBanner();
|
this.updateIOSAudioBanner();
|
||||||
@@ -499,6 +486,8 @@ class MotifApp {
|
|||||||
this.startMotifProgressUpdates();
|
this.startMotifProgressUpdates();
|
||||||
|
|
||||||
this.updateStatus('');
|
this.updateStatus('');
|
||||||
|
this.hasGenerated = true;
|
||||||
|
this.setState('generated');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.updateStatus(`Motif error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
this.updateStatus(`Motif error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
this.motifBtn.disabled = false;
|
this.motifBtn.disabled = false;
|
||||||
@@ -591,20 +580,17 @@ class MotifApp {
|
|||||||
return this.titleCase(s);
|
return this.titleCase(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
private hideResults(): void {
|
|
||||||
this.resultsSection.classList.remove('visible');
|
|
||||||
this.playerSection.classList.remove('visible');
|
|
||||||
}
|
|
||||||
|
|
||||||
private enablePlayerControls(): void {
|
private enablePlayerControls(): void {
|
||||||
this.motifBtn.disabled = false;
|
this.motifBtn.disabled = false;
|
||||||
this.copyLinkBtn.disabled = this.currentMIDI == null;
|
this.copyLinkBtn.disabled = this.currentMIDI == null;
|
||||||
|
this.previewBtn.disabled = this.currentMIDI == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private disablePlayerControls(): void {
|
private disablePlayerControls(): void {
|
||||||
this.motifBtn.disabled = true;
|
this.motifBtn.disabled = true;
|
||||||
this.motifStopBtn.disabled = true;
|
this.motifStopBtn.disabled = true;
|
||||||
this.copyLinkBtn.disabled = true;
|
this.copyLinkBtn.disabled = true;
|
||||||
|
this.previewBtn.disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateStatus(message: string): void {
|
private updateStatus(message: string): void {
|
||||||
@@ -638,6 +624,7 @@ class MotifApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async handleCopyLink(): Promise<void> {
|
private async handleCopyLink(): Promise<void> {
|
||||||
|
if (!this.hasGenerated) return;
|
||||||
const result = this.searchResults[this.selectedResultIndex];
|
const result = this.searchResults[this.selectedResultIndex];
|
||||||
if (!result?.midiUrl) return;
|
if (!result?.midiUrl) return;
|
||||||
|
|
||||||
@@ -694,6 +681,33 @@ class MotifApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setState(state: 'idle' | 'results' | 'selected' | 'generated'): void {
|
||||||
|
// results
|
||||||
|
const hasResults = state === 'results' || state === 'selected' || state === 'generated';
|
||||||
|
this.resultsSection.classList.toggle('visible', hasResults);
|
||||||
|
this.chooseHelper.style.display = state === 'results' ? 'block' : 'none';
|
||||||
|
|
||||||
|
// selected card
|
||||||
|
const hasSelection = state === 'selected' || state === 'generated';
|
||||||
|
this.playerSection.classList.toggle('visible', hasSelection);
|
||||||
|
|
||||||
|
// share link only after generation
|
||||||
|
this.copyLinkBtn.style.display = state === 'generated' ? 'inline-block' : 'none';
|
||||||
|
this.copyLinkBtn.disabled = state !== 'generated';
|
||||||
|
|
||||||
|
// collapse results when selected/generated
|
||||||
|
this.resultsSection.classList.toggle('collapsed', state === 'selected' || state === 'generated');
|
||||||
|
}
|
||||||
|
|
||||||
|
private goToResults(): void {
|
||||||
|
this.setState('results');
|
||||||
|
try {
|
||||||
|
this.resultsSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private updateEmbedSnippet(songTitle: string): void {
|
private updateEmbedSnippet(songTitle: string): void {
|
||||||
if (!this.embedSection || !this.embedCodeEl) return;
|
if (!this.embedSection || !this.embedCodeEl) return;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user