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:
+17
-4
@@ -522,7 +522,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.preview-controls button.next-result {
|
.preview-controls button.next-result {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-controls .preview-actions {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
display: inline-flex;
|
||||||
|
gap: calc(var(--spacing-unit) * 1.25);
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-controls .volume-control {
|
.preview-controls .volume-control {
|
||||||
@@ -537,8 +544,11 @@
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-controls button.next-result {
|
.preview-controls .preview-actions {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -864,9 +874,12 @@
|
|||||||
<button id="soundfontStopBtn" disabled>Stop</button>
|
<button id="soundfontStopBtn" disabled>Stop</button>
|
||||||
<div class="volume-control">
|
<div class="volume-control">
|
||||||
<label for="soundfontVolume">Volume</label>
|
<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>
|
||||||
<button id="nextResultBtn" class="next-result" disabled>Try Next Result</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -889,7 +902,7 @@
|
|||||||
<button id="motifStopBtn" disabled>Stop</button>
|
<button id="motifStopBtn" disabled>Stop</button>
|
||||||
<div class="volume-control">
|
<div class="volume-control">
|
||||||
<label for="motifVolume">Volume</label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+89
@@ -45,6 +45,7 @@ class MotifApp {
|
|||||||
private iosAudioState!: HTMLElement;
|
private iosAudioState!: HTMLElement;
|
||||||
|
|
||||||
private nextResultBtn!: HTMLButtonElement;
|
private nextResultBtn!: HTMLButtonElement;
|
||||||
|
private copyLinkBtn!: HTMLButtonElement;
|
||||||
|
|
||||||
// Embed snippet UI
|
// Embed snippet UI
|
||||||
private embedSection: HTMLElement | null = null;
|
private embedSection: HTMLElement | null = null;
|
||||||
@@ -68,6 +69,9 @@ class MotifApp {
|
|||||||
|
|
||||||
this.initializeUI();
|
this.initializeUI();
|
||||||
this.setupEventListeners();
|
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();
|
const audioContext = await unlockAudio();
|
||||||
if (!this.soundfontPlayer) {
|
if (!this.soundfontPlayer) {
|
||||||
this.soundfontPlayer = new SoundfontMIDIPlayer(audioContext);
|
this.soundfontPlayer = new SoundfontMIDIPlayer(audioContext);
|
||||||
|
this.soundfontPlayer.setVolume(parseFloat(this.soundfontVolumeSlider.value));
|
||||||
}
|
}
|
||||||
return this.soundfontPlayer;
|
return this.soundfontPlayer;
|
||||||
}
|
}
|
||||||
@@ -110,6 +115,7 @@ class MotifApp {
|
|||||||
this.motifDuration = document.getElementById('motifDuration')!;
|
this.motifDuration = document.getElementById('motifDuration')!;
|
||||||
|
|
||||||
this.nextResultBtn = document.getElementById('nextResultBtn') as HTMLButtonElement;
|
this.nextResultBtn = document.getElementById('nextResultBtn') as HTMLButtonElement;
|
||||||
|
this.copyLinkBtn = document.getElementById('copyLinkBtn') as HTMLButtonElement;
|
||||||
|
|
||||||
// iOS audio unlock UI (Motif)
|
// iOS audio unlock UI (Motif)
|
||||||
this.iosAudioBanner = document.getElementById('iosAudioBanner')!;
|
this.iosAudioBanner = document.getElementById('iosAudioBanner')!;
|
||||||
@@ -161,6 +167,7 @@ class MotifApp {
|
|||||||
this.motifProgressBar.addEventListener('change', seekHandler);
|
this.motifProgressBar.addEventListener('change', seekHandler);
|
||||||
|
|
||||||
this.nextResultBtn.addEventListener('click', () => this.handleNextResult());
|
this.nextResultBtn.addEventListener('click', () => this.handleNextResult());
|
||||||
|
this.copyLinkBtn.addEventListener('click', () => void this.handleCopyLink());
|
||||||
|
|
||||||
// Embed snippet copy (may be disabled / not-live)
|
// Embed snippet copy (may be disabled / not-live)
|
||||||
this.copyEmbedBtn?.addEventListener('click', () => void this.copyEmbedSnippet());
|
this.copyEmbedBtn?.addEventListener('click', () => void this.copyEmbedSnippet());
|
||||||
@@ -359,6 +366,7 @@ class MotifApp {
|
|||||||
// Load into preview player (ensure audio unlocked on iOS)
|
// Load into preview player (ensure audio unlocked on iOS)
|
||||||
const player = await this.ensureAudioReady();
|
const player = await this.ensureAudioReady();
|
||||||
await player.load(events);
|
await player.load(events);
|
||||||
|
player.setVolume(parseFloat(this.soundfontVolumeSlider.value));
|
||||||
|
|
||||||
// Update UI
|
// Update UI
|
||||||
this.selectedTitle.textContent = result.title;
|
this.selectedTitle.textContent = result.title;
|
||||||
@@ -505,6 +513,7 @@ class MotifApp {
|
|||||||
this.soundfontPlayBtn.disabled = false;
|
this.soundfontPlayBtn.disabled = false;
|
||||||
this.motifBtn.disabled = false;
|
this.motifBtn.disabled = false;
|
||||||
this.nextResultBtn.disabled = this.searchResults.length <= 1;
|
this.nextResultBtn.disabled = this.searchResults.length <= 1;
|
||||||
|
this.copyLinkBtn.disabled = this.searchResults.length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private disablePlayerControls(): void {
|
private disablePlayerControls(): void {
|
||||||
@@ -513,12 +522,92 @@ class MotifApp {
|
|||||||
this.motifBtn.disabled = true;
|
this.motifBtn.disabled = true;
|
||||||
this.motifStopBtn.disabled = true;
|
this.motifStopBtn.disabled = true;
|
||||||
this.nextResultBtn.disabled = true;
|
this.nextResultBtn.disabled = true;
|
||||||
|
this.copyLinkBtn.disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateStatus(message: string): void {
|
private updateStatus(message: string): void {
|
||||||
this.status.textContent = message;
|
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 {
|
private updateEmbedSnippet(songTitle: string): void {
|
||||||
if (!this.embedSection || !this.embedCodeEl) return;
|
if (!this.embedSection || !this.embedCodeEl) return;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user