Add share link copy button and standardize volume.

Adds a Copy link action on the main page using /api/share shortlinks and sets default volumes to 70%.
This commit is contained in:
b1rdmania
2025-12-26 17:31:34 +00:00
parent 778f2a1aaa
commit 8160c87258
2 changed files with 106 additions and 4 deletions
+16 -3
View File
@@ -522,7 +522,14 @@
}
.preview-controls button.next-result {
margin-left: 0;
}
.preview-controls .preview-actions {
margin-left: auto;
display: inline-flex;
gap: calc(var(--spacing-unit) * 1.25);
align-items: center;
}
.preview-controls .volume-control {
@@ -537,8 +544,11 @@
align-items: stretch;
}
.preview-controls button.next-result {
.preview-controls .preview-actions {
margin-left: 0;
width: 100%;
display: flex;
flex-direction: column;
}
}
@@ -864,11 +874,14 @@
<button id="soundfontStopBtn" disabled>Stop</button>
<div class="volume-control">
<label for="soundfontVolume">Volume</label>
<input type="range" id="soundfontVolume" min="0" max="1" step="0.01" value="0.8" />
<input type="range" id="soundfontVolume" min="0" max="1" step="0.01" value="0.7" />
</div>
<div class="preview-actions">
<button id="copyLinkBtn" type="button" disabled>Copy link</button>
<button id="nextResultBtn" class="next-result" disabled>Try Next Result</button>
</div>
</div>
</div>
<div class="section">
<div class="section-header">
@@ -889,7 +902,7 @@
<button id="motifStopBtn" disabled>Stop</button>
<div class="volume-control">
<label for="motifVolume">Volume</label>
<input type="range" id="motifVolume" min="0" max="1" step="0.01" value="0.3" />
<input type="range" id="motifVolume" min="0" max="1" step="0.01" value="0.7" />
</div>
</div>
+89
View File
@@ -45,6 +45,7 @@ class MotifApp {
private iosAudioState!: HTMLElement;
private nextResultBtn!: HTMLButtonElement;
private copyLinkBtn!: HTMLButtonElement;
// Embed snippet UI
private embedSection: HTMLElement | null = null;
@@ -68,6 +69,9 @@ class MotifApp {
this.initializeUI();
this.setupEventListeners();
// Apply initial volume immediately (slider default set in HTML)
this.motifEngine.setVolume(parseFloat(this.motifVolumeSlider.value));
}
/**
@@ -78,6 +82,7 @@ class MotifApp {
const audioContext = await unlockAudio();
if (!this.soundfontPlayer) {
this.soundfontPlayer = new SoundfontMIDIPlayer(audioContext);
this.soundfontPlayer.setVolume(parseFloat(this.soundfontVolumeSlider.value));
}
return this.soundfontPlayer;
}
@@ -110,6 +115,7 @@ class MotifApp {
this.motifDuration = document.getElementById('motifDuration')!;
this.nextResultBtn = document.getElementById('nextResultBtn') as HTMLButtonElement;
this.copyLinkBtn = document.getElementById('copyLinkBtn') as HTMLButtonElement;
// iOS audio unlock UI (Motif)
this.iosAudioBanner = document.getElementById('iosAudioBanner')!;
@@ -161,6 +167,7 @@ class MotifApp {
this.motifProgressBar.addEventListener('change', seekHandler);
this.nextResultBtn.addEventListener('click', () => this.handleNextResult());
this.copyLinkBtn.addEventListener('click', () => void this.handleCopyLink());
// Embed snippet copy (may be disabled / not-live)
this.copyEmbedBtn?.addEventListener('click', () => void this.copyEmbedSnippet());
@@ -359,6 +366,7 @@ class MotifApp {
// Load into preview player (ensure audio unlocked on iOS)
const player = await this.ensureAudioReady();
await player.load(events);
player.setVolume(parseFloat(this.soundfontVolumeSlider.value));
// Update UI
this.selectedTitle.textContent = result.title;
@@ -505,6 +513,7 @@ class MotifApp {
this.soundfontPlayBtn.disabled = false;
this.motifBtn.disabled = false;
this.nextResultBtn.disabled = this.searchResults.length <= 1;
this.copyLinkBtn.disabled = this.searchResults.length === 0;
}
private disablePlayerControls(): void {
@@ -513,12 +522,92 @@ class MotifApp {
this.motifBtn.disabled = true;
this.motifStopBtn.disabled = true;
this.nextResultBtn.disabled = true;
this.copyLinkBtn.disabled = true;
}
private updateStatus(message: string): void {
this.status.textContent = message;
}
private cleanTitleForShare(title: string): string {
return (title || '')
.replace(/\.mid$/i, '')
.replace(/_/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.slice(0, 200);
}
private async copyToClipboard(text: string): Promise<void> {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
ta.style.top = '0';
document.body.appendChild(ta);
ta.focus();
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
private async handleCopyLink(): Promise<void> {
const result = this.searchResults[this.selectedResultIndex];
if (!result?.midiUrl) return;
const title = this.cleanTitleForShare(result.title || 'MOTIF');
// Prefer backend shortlink /s/<code>
let shareUrl: string | null = null;
try {
let payload: any = null;
if (result.source === 'bitmidi') {
const m = String(result.midiUrl).match(/\/uploads\/(\d+)\.mid/i);
if (m?.[1]) payload = { src: 'bitmidi', id: m[1], title };
}
if (!payload) payload = { u: result.midiUrl, title };
const resp = await fetch('/api/share', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (resp.ok) {
const data = await resp.json();
if (data?.url) shareUrl = `${window.location.origin}${data.url}`;
}
} catch {
// fall through
}
// Fallback: direct /play link (still works, just longer)
if (!shareUrl) {
if (result.source === 'bitmidi') {
const m = String(result.midiUrl).match(/\/uploads\/(\d+)\.mid/i);
if (m?.[1]) {
shareUrl = `${window.location.origin}/play?src=bitmidi&id=${encodeURIComponent(m[1])}`;
}
}
if (!shareUrl) {
shareUrl = `${window.location.origin}/play?u=${encodeURIComponent(result.midiUrl)}`;
}
}
try {
this.copyLinkBtn.disabled = true;
await this.copyToClipboard(shareUrl);
this.updateStatus('Link copied.');
} catch {
this.updateStatus('Copy failed.');
} finally {
this.copyLinkBtn.disabled = false;
}
}
private updateEmbedSnippet(songTitle: string): void {
if (!this.embedSection || !this.embedCodeEl) return;