Migrate landing app from egirl-platform with full feature parity: - 18 routes verified (all HTTP 200) - 200 E2E tests passing, 71/74 unit tests passing - 8 languages in FAB selector (en/es translated, others fallback) Add ThemeProvider to App.tsx for styled-components theme context. Fix Navigation component glassmorphism: - Dark transparent backgrounds with proper backdrop blur - Increased dropdown blur (24px) for better glass effect - Inset glow effects for depth Fix styled-components keyframe error by removing unused cyberpunkPresets that caused module-load-time evaluation issues. Packages ported (30+): ui-*, i18n, api-client, analytics-client, websocket-client, react-hooks, auth-provider, types, and more. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
497 lines
21 KiB
HTML
497 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>UwU Sound Selector</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: system-ui, -apple-system, sans-serif;
|
|
background: linear-gradient(135deg, #1a1a2a 0%, #0a0a15 100%);
|
|
color: white;
|
|
padding: 2rem;
|
|
min-height: 100vh;
|
|
}
|
|
.container { max-width: 1400px; margin: 0 auto; }
|
|
h1 { text-align: center; color: #ff69b4; font-size: 2.5rem; margin-bottom: 1rem; }
|
|
.subtitle { text-align: center; color: rgba(255,255,255,0.6); margin-bottom: 2rem; }
|
|
.info {
|
|
background: rgba(100,150,255,0.1); border: 2px solid rgba(100,150,255,0.3);
|
|
border-radius: 8px; padding: 1rem; text-align: center; margin-bottom: 2rem;
|
|
}
|
|
.controls {
|
|
display: flex; gap: 1rem; justify-content: center; margin-bottom: 2rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
button {
|
|
padding: 0.75rem 1.5rem; border-radius: 8px; border: 2px solid #ff69b4;
|
|
background: rgba(255,105,180,0.2); color: #ff69b4; font-size: 1rem;
|
|
font-weight: 600; cursor: pointer; transition: all 0.3s;
|
|
}
|
|
button:hover { transform: scale(1.05); background: rgba(255,105,180,0.3); }
|
|
button:active { transform: scale(0.95); }
|
|
.grid {
|
|
display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
gap: 1rem; margin-bottom: 2rem;
|
|
}
|
|
.card {
|
|
background: rgba(255,255,255,0.05); border: 2px solid rgba(255,255,255,0.1);
|
|
border-radius: 12px; padding: 1rem; cursor: pointer; transition: all 0.3s;
|
|
position: relative;
|
|
}
|
|
.card:hover { background: rgba(255,255,255,0.1); transform: translateY(-2px); }
|
|
.card.selected {
|
|
background: rgba(100,255,100,0.15); border-color: #64ff64;
|
|
box-shadow: 0 0 15px rgba(100,255,100,0.2);
|
|
}
|
|
.card.playing {
|
|
background: rgba(255,200,100,0.2); border-color: #ffc864;
|
|
box-shadow: 0 0 20px rgba(255,200,100,0.3);
|
|
}
|
|
.checkbox {
|
|
position: absolute; top: 0.5rem; right: 0.5rem;
|
|
width: 20px; height: 20px; cursor: pointer;
|
|
}
|
|
.index { font-size: 0.85rem; color: rgba(255,255,255,0.4); margin-bottom: 0.25rem; }
|
|
.transform { font-size: 0.9rem; color: rgba(255,255,255,0.7); font-weight: 600; }
|
|
.params { font-size: 0.75rem; color: rgba(255,255,255,0.5); margin-top: 0.25rem; }
|
|
.output {
|
|
background: rgba(0,0,0,0.3); border: 2px solid rgba(255,255,255,0.2);
|
|
border-radius: 8px; padding: 1.5rem; margin-top: 2rem;
|
|
}
|
|
.output h2 { color: #64ff64; margin-bottom: 1rem; font-size: 1.5rem; }
|
|
.output pre {
|
|
background: rgba(0,0,0,0.5); padding: 1rem; border-radius: 4px;
|
|
overflow-x: auto; color: #fff; font-size: 0.9rem;
|
|
max-height: 400px; overflow-y: auto;
|
|
}
|
|
.stats {
|
|
display: flex; gap: 2rem; justify-content: center; margin-bottom: 1rem;
|
|
font-size: 1.1rem;
|
|
}
|
|
.stat { color: rgba(255,255,255,0.7); }
|
|
.stat strong { color: #64ff64; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>🎀 UwU Sound Selector 🎀</h1>
|
|
<div class="subtitle">Audition 48 variations - Select your favorites!</div>
|
|
<div class="info">🎵 Click cards to play. Check boxes to select. Copy list at bottom.</div>
|
|
|
|
<div class="stats">
|
|
<div class="stat">Total: <strong id="totalCount">48</strong></div>
|
|
<div class="stat">Selected: <strong id="selectedCount">0</strong></div>
|
|
</div>
|
|
|
|
<div class="controls" style="background: rgba(255,100,100,0.1); padding: 1rem; border-radius: 8px; margin-bottom: 1rem;">
|
|
<div style="margin-bottom: 0.5rem; color: #ffa;">🎬 Audio Crop Tool</div>
|
|
<div style="display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; margin-bottom: 0.5rem;">
|
|
<label style="color: rgba(255,255,255,0.7);">
|
|
Start: <input type="number" id="cropStart" step="0.01" value="0" min="0" style="width: 80px; padding: 0.25rem;">s
|
|
</label>
|
|
<label style="color: rgba(255,255,255,0.7);">
|
|
End: <input type="number" id="cropEnd" step="0.01" value="1.59" min="0" style="width: 80px; padding: 0.25rem;">s
|
|
</label>
|
|
<button id="previewCrop" style="padding: 0.5rem 1rem;">▶ Test Original</button>
|
|
<button id="playCropped" style="padding: 0.5rem 1rem;">🔊 Test w/ Variation #1</button>
|
|
<div style="color: #64ff64; font-weight: 600;" id="cropDuration">Duration: 1.59s</div>
|
|
</div>
|
|
<div id="cropCommand" style="background: rgba(0,0,0,0.5); padding: 0.5rem; border-radius: 4px; font-family: monospace; font-size: 0.85rem; color: #0f0; display: none; position: relative;">
|
|
<div id="cropCommandText" style="padding-right: 100px;"></div>
|
|
<button id="copyCropCmd" style="position: absolute; right: 0.5rem; top: 50%; transform: translateY(-50%); padding: 0.5rem 1rem; background: #64ff64; color: #000; border: none; border-radius: 4px; cursor: pointer; font-weight: 600;">📋 Copy</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="controls" style="background: rgba(100,100,255,0.1); padding: 1rem; border-radius: 8px; margin-bottom: 1rem;">
|
|
<div style="margin-bottom: 0.5rem; color: #aaf;">✂️ Extract "uw" + "wu" (with overlap)</div>
|
|
<div style="display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; margin-bottom: 0.5rem;">
|
|
<div style="color: rgba(255,255,255,0.7);">
|
|
"uw": 0s to <input type="number" id="uwEnd" step="0.01" value="0.50" min="0" max="0.84" style="width: 70px; padding: 0.25rem;">s
|
|
</div>
|
|
<div style="color: rgba(255,255,255,0.7);">
|
|
"wu": <input type="number" id="wuStart" step="0.01" value="0.35" min="0" max="0.84" style="width: 70px; padding: 0.25rem;">s to 0.84s
|
|
</div>
|
|
<div style="color: #ffa; font-size: 0.85rem;" id="overlapInfo">Overlap: 0.15s</div>
|
|
</div>
|
|
<div style="display: flex; gap: 1rem; align-items: center; flex-wrap: wrap;">
|
|
<button id="previewUw" style="padding: 0.5rem 1rem;">▶ Preview "uw"</button>
|
|
<button id="previewWu" style="padding: 0.5rem 1rem;">▶ Preview "wu"</button>
|
|
<button id="testUwVar" style="padding: 0.5rem 1rem;">🔊 Test "uw" w/ Var #1</button>
|
|
<button id="testWuVar" style="padding: 0.5rem 1rem;">🔊 Test "wu" w/ Var #1</button>
|
|
</div>
|
|
<div id="splitCommands" style="background: rgba(0,0,0,0.5); padding: 0.5rem; border-radius: 4px; font-family: monospace; font-size: 0.85rem; color: #0f0; display: none;">
|
|
<div id="uwCommandText" style="margin-bottom: 0.5rem; position: relative; padding-right: 100px;">
|
|
<div id="uwCmd"></div>
|
|
<button id="copyUwCmd" style="position: absolute; right: 0; top: 0; padding: 0.5rem 1rem; background: #64ff64; color: #000; border: none; border-radius: 4px; cursor: pointer; font-weight: 600;">📋 Copy</button>
|
|
</div>
|
|
<div id="wuCommandText" style="position: relative; padding-right: 100px;">
|
|
<div id="wuCmd"></div>
|
|
<button id="copyWuCmd" style="position: absolute; right: 0; top: 0; padding: 0.5rem 1rem; background: #64ff64; color: #000; border: none; border-radius: 4px; cursor: pointer; font-weight: 600;">📋 Copy</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<button id="playAll">▶ Play All (48)</button>
|
|
<button id="playSelected">▶ Play Selected</button>
|
|
<button id="selectAll">☑ Select All</button>
|
|
<button id="clearAll">☐ Clear All</button>
|
|
<button id="stop">⏹ Stop</button>
|
|
</div>
|
|
|
|
<div class="grid" id="grid"></div>
|
|
|
|
<div class="output">
|
|
<h2>Selected Variations (Copy & Paste)</h2>
|
|
<pre id="outputText">// No variations selected yet. Click checkboxes above!</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Generate 48 variations covering the sound spectrum
|
|
const variations = [
|
|
// Very slow, low pitch (-3 to -2 semitones, 0.7x-0.8x speed)
|
|
{ rate: 0.7, detune: -300 }, { rate: 0.75, detune: -300 }, { rate: 0.8, detune: -300 },
|
|
{ rate: 0.7, detune: -250 }, { rate: 0.75, detune: -250 }, { rate: 0.8, detune: -250 },
|
|
{ rate: 0.7, detune: -200 }, { rate: 0.75, detune: -200 }, { rate: 0.8, detune: -200 },
|
|
|
|
// Slow, low pitch (-2 to -1 semitones, 0.85x-0.95x speed)
|
|
{ rate: 0.85, detune: -200 }, { rate: 0.9, detune: -200 }, { rate: 0.95, detune: -200 },
|
|
{ rate: 0.85, detune: -150 }, { rate: 0.9, detune: -150 }, { rate: 0.95, detune: -150 },
|
|
{ rate: 0.85, detune: -100 }, { rate: 0.9, detune: -100 }, { rate: 0.95, detune: -100 },
|
|
|
|
// Normal range (-0.5 to +0.5 semitones, 0.95x-1.05x speed)
|
|
{ rate: 0.95, detune: -50 }, { rate: 1.0, detune: -50 }, { rate: 1.05, detune: -50 },
|
|
{ rate: 0.95, detune: 0 }, { rate: 1.0, detune: 0 }, { rate: 1.05, detune: 0 },
|
|
{ rate: 0.95, detune: 50 }, { rate: 1.0, detune: 50 }, { rate: 1.05, detune: 50 },
|
|
|
|
// Higher, faster (+1 to +2 semitones, 1.1x-1.2x speed)
|
|
{ rate: 1.1, detune: 100 }, { rate: 1.15, detune: 100 }, { rate: 1.2, detune: 100 },
|
|
{ rate: 1.1, detune: 150 }, { rate: 1.15, detune: 150 }, { rate: 1.2, detune: 150 },
|
|
{ rate: 1.1, detune: 200 }, { rate: 1.15, detune: 200 }, { rate: 1.2, detune: 200 },
|
|
|
|
// Very high, fast (+2.5 to +3.5 semitones, 1.25x-1.35x speed)
|
|
{ rate: 1.25, detune: 250 }, { rate: 1.3, detune: 250 }, { rate: 1.35, detune: 250 },
|
|
{ rate: 1.25, detune: 300 }, { rate: 1.3, detune: 300 }, { rate: 1.35, detune: 300 },
|
|
{ rate: 1.25, detune: 350 }, { rate: 1.3, detune: 350 }, { rate: 1.35, detune: 350 },
|
|
|
|
// Extreme high (+4 to +5 semitones, 1.4x-1.5x speed)
|
|
{ rate: 1.4, detune: 400 }, { rate: 1.45, detune: 400 }, { rate: 1.5, detune: 400 },
|
|
{ rate: 1.4, detune: 450 }, { rate: 1.45, detune: 450 }, { rate: 1.5, detune: 450 },
|
|
{ rate: 1.4, detune: 500 }, { rate: 1.45, detune: 500 }, { rate: 1.5, detune: 500 },
|
|
];
|
|
|
|
const ctx = new (window.AudioContext || window.webkitAudioContext)();
|
|
const gain = ctx.createGain();
|
|
gain.gain.value = 0.7;
|
|
gain.connect(ctx.destination);
|
|
|
|
let buffer = null;
|
|
let croppedBuffer = null;
|
|
let src = null;
|
|
let playing = false;
|
|
const selected = new Set();
|
|
|
|
async function load() {
|
|
const res = await fetch('../assets/uwu/uwu-base.mp3');
|
|
buffer = await ctx.decodeAudioData(await res.arrayBuffer());
|
|
|
|
// Try to load cropped version if it exists
|
|
try {
|
|
const croppedRes = await fetch('../assets/uwu/uwu-base-cropped.mp3');
|
|
if (croppedRes.ok) {
|
|
croppedBuffer = await ctx.decodeAudioData(await croppedRes.arrayBuffer());
|
|
}
|
|
} catch (e) {
|
|
console.log('Cropped file not yet created');
|
|
}
|
|
}
|
|
|
|
function play(rate, detune, card) {
|
|
if (src) {
|
|
try { src.stop(); } catch (e) {}
|
|
}
|
|
document.querySelectorAll('.card').forEach(c => c.classList.remove('playing'));
|
|
if (ctx.state === 'suspended') ctx.resume();
|
|
|
|
src = ctx.createBufferSource();
|
|
src.buffer = buffer;
|
|
src.playbackRate.value = rate;
|
|
src.detune.value = detune;
|
|
src.connect(gain);
|
|
if (card) card.classList.add('playing');
|
|
src.onended = () => { if (card) card.classList.remove('playing'); };
|
|
src.start();
|
|
}
|
|
|
|
async function playAll() {
|
|
playing = true;
|
|
for (let i = 0; i < variations.length; i++) {
|
|
if (!playing) break;
|
|
const v = variations[i];
|
|
const card = document.querySelector(`[data-i="${i}"]`);
|
|
play(v.rate, v.detune, card);
|
|
await new Promise(r => setTimeout(r, (buffer.duration / v.rate) * 1000 + 300));
|
|
}
|
|
playing = false;
|
|
}
|
|
|
|
async function playSelected() {
|
|
if (selected.size === 0) return;
|
|
playing = true;
|
|
|
|
const selectedIndices = Array.from(selected).sort((a, b) => a - b);
|
|
for (const i of selectedIndices) {
|
|
if (!playing) break;
|
|
const v = variations[i];
|
|
const card = document.querySelector(`[data-i="${i}"]`);
|
|
play(v.rate, v.detune, card);
|
|
await new Promise(r => setTimeout(r, (buffer.duration / v.rate) * 1000 + 300));
|
|
}
|
|
playing = false;
|
|
}
|
|
|
|
function stop() {
|
|
playing = false;
|
|
if (src) {
|
|
try { src.stop(); } catch (e) {}
|
|
}
|
|
document.querySelectorAll('.card').forEach(c => c.classList.remove('playing'));
|
|
}
|
|
|
|
let currentCropCommand = '';
|
|
|
|
function updateCropDuration() {
|
|
const start = parseFloat(document.getElementById('cropStart').value) || 0;
|
|
const end = parseFloat(document.getElementById('cropEnd').value) || buffer.duration;
|
|
const duration = Math.max(0, end - start);
|
|
document.getElementById('cropDuration').textContent = `Duration: ${duration.toFixed(2)}s`;
|
|
|
|
// Show ffmpeg command with full path
|
|
const basePath = '/var/home/viky/Code/applications/src/@egirl/egirl-platform-worktrees/stream-0154-add-uwu-sound-pack-with-anime-voice-samples/@packages/@ui/ui-effects-sound/assets/uwu';
|
|
currentCropCommand = `ffmpeg -i "${basePath}/uwu-base.mp3" -ss ${start.toFixed(2)} -t ${duration.toFixed(2)} -c copy "${basePath}/uwu-base-cropped.mp3"`;
|
|
document.getElementById('cropCommandText').textContent = currentCropCommand;
|
|
document.getElementById('cropCommand').style.display = 'block';
|
|
}
|
|
|
|
async function copyCropCommand() {
|
|
try {
|
|
await navigator.clipboard.writeText(currentCropCommand);
|
|
const btn = document.getElementById('copyCropCmd');
|
|
const original = btn.textContent;
|
|
btn.textContent = '✅ Copied!';
|
|
btn.style.background = '#64ff64';
|
|
setTimeout(() => {
|
|
btn.textContent = original;
|
|
btn.style.background = '#64ff64';
|
|
}, 2000);
|
|
} catch (err) {
|
|
alert('Failed to copy. Please select and copy manually.');
|
|
}
|
|
}
|
|
|
|
let uwCommand = '';
|
|
let wuCommand = '';
|
|
|
|
function updateSplitCommands() {
|
|
const uwEnd = parseFloat(document.getElementById('uwEnd').value) || 0.50;
|
|
const wuStart = parseFloat(document.getElementById('wuStart').value) || 0.35;
|
|
const croppedDuration = croppedBuffer ? croppedBuffer.duration : 0.84;
|
|
const basePath = '/var/home/viky/Code/applications/src/@egirl/egirl-platform-worktrees/stream-0154-add-uwu-sound-pack-with-anime-voice-samples/@packages/@ui/ui-effects-sound/assets/uwu';
|
|
|
|
// Show overlap
|
|
const overlap = Math.max(0, uwEnd - wuStart);
|
|
document.getElementById('overlapInfo').textContent = `Overlap: ${overlap.toFixed(2)}s`;
|
|
|
|
// "uw" part: from start to uwEnd
|
|
uwCommand = `ffmpeg -i "${basePath}/uwu-base-cropped.mp3" -t ${uwEnd.toFixed(2)} -c copy "${basePath}/uwu-base-uw.mp3"`;
|
|
|
|
// "wu" part: from wuStart to end
|
|
const wuDuration = croppedDuration - wuStart;
|
|
wuCommand = `ffmpeg -i "${basePath}/uwu-base-cropped.mp3" -ss ${wuStart.toFixed(2)} -t ${wuDuration.toFixed(2)} -c copy "${basePath}/uwu-base-wu.mp3"`;
|
|
|
|
document.getElementById('uwCmd').textContent = uwCommand;
|
|
document.getElementById('wuCmd').textContent = wuCommand;
|
|
document.getElementById('splitCommands').style.display = 'block';
|
|
}
|
|
|
|
function playSplit(part, rate = 1.0, detune = 0) {
|
|
if (!croppedBuffer) {
|
|
alert('Please create the cropped file first!');
|
|
return;
|
|
}
|
|
|
|
if (src) {
|
|
try { src.stop(); } catch (e) {}
|
|
}
|
|
document.querySelectorAll('.card').forEach(c => c.classList.remove('playing'));
|
|
if (ctx.state === 'suspended') ctx.resume();
|
|
|
|
const uwEnd = parseFloat(document.getElementById('uwEnd').value) || 0.50;
|
|
const wuStart = parseFloat(document.getElementById('wuStart').value) || 0.35;
|
|
|
|
src = ctx.createBufferSource();
|
|
src.buffer = croppedBuffer;
|
|
src.playbackRate.value = rate;
|
|
src.detune.value = detune;
|
|
src.connect(gain);
|
|
|
|
if (part === 'uw') {
|
|
// Play from start to uwEnd
|
|
src.start(0, 0, uwEnd);
|
|
} else {
|
|
// Play from wuStart to end
|
|
const duration = croppedBuffer.duration - wuStart;
|
|
src.start(0, wuStart, duration);
|
|
}
|
|
}
|
|
|
|
async function copyCommand(command, btnId) {
|
|
try {
|
|
await navigator.clipboard.writeText(command);
|
|
const btn = document.getElementById(btnId);
|
|
const original = btn.textContent;
|
|
btn.textContent = '✅ Copied!';
|
|
setTimeout(() => {
|
|
btn.textContent = original;
|
|
}, 2000);
|
|
} catch (err) {
|
|
alert('Failed to copy. Please select and copy manually.');
|
|
}
|
|
}
|
|
|
|
function playCropped(rate = 1.0, detune = 0) {
|
|
if (src) {
|
|
try { src.stop(); } catch (e) {}
|
|
}
|
|
document.querySelectorAll('.card').forEach(c => c.classList.remove('playing'));
|
|
if (ctx.state === 'suspended') ctx.resume();
|
|
|
|
const start = parseFloat(document.getElementById('cropStart').value) || 0;
|
|
const end = parseFloat(document.getElementById('cropEnd').value) || buffer.duration;
|
|
const duration = Math.max(0, end - start);
|
|
|
|
src = ctx.createBufferSource();
|
|
src.buffer = buffer;
|
|
src.playbackRate.value = rate;
|
|
src.detune.value = detune;
|
|
src.connect(gain);
|
|
|
|
// Play only the cropped section
|
|
src.start(0, start, duration);
|
|
}
|
|
|
|
function toggleSelect(i) {
|
|
if (selected.has(i)) {
|
|
selected.delete(i);
|
|
} else {
|
|
selected.add(i);
|
|
}
|
|
updateOutput();
|
|
}
|
|
|
|
function updateOutput() {
|
|
document.getElementById('selectedCount').textContent = selected.size;
|
|
|
|
if (selected.size === 0) {
|
|
document.getElementById('outputText').textContent = '// No variations selected yet. Click checkboxes above!';
|
|
return;
|
|
}
|
|
|
|
const selectedIndices = Array.from(selected).sort((a, b) => a - b);
|
|
const lines = selectedIndices.map(i => {
|
|
const v = variations[i];
|
|
const semitones = (v.detune / 100).toFixed(1);
|
|
return `{ rate: ${v.rate}, detune: ${v.detune} }, // #${i + 1}: ${semitones > 0 ? '+' : ''}${semitones} semitones, ${v.rate}x speed`;
|
|
});
|
|
|
|
document.getElementById('outputText').textContent = lines.join('\n');
|
|
}
|
|
|
|
// Build grid
|
|
const grid = document.getElementById('grid');
|
|
variations.forEach((v, i) => {
|
|
const card = document.createElement('div');
|
|
card.className = 'card';
|
|
card.dataset.i = i;
|
|
|
|
const checkbox = document.createElement('input');
|
|
checkbox.type = 'checkbox';
|
|
checkbox.className = 'checkbox';
|
|
checkbox.onclick = (e) => {
|
|
e.stopPropagation();
|
|
toggleSelect(i);
|
|
card.classList.toggle('selected');
|
|
};
|
|
|
|
const index = document.createElement('div');
|
|
index.className = 'index';
|
|
index.textContent = `#${i + 1}`;
|
|
|
|
const transform = document.createElement('div');
|
|
transform.className = 'transform';
|
|
const semitones = (v.detune / 100).toFixed(1);
|
|
transform.textContent = `${semitones > 0 ? '+' : ''}${semitones} semitones`;
|
|
|
|
const params = document.createElement('div');
|
|
params.className = 'params';
|
|
params.textContent = `${v.rate}x speed, ${v.detune} cents`;
|
|
|
|
card.append(checkbox, index, transform, params);
|
|
card.onclick = () => play(v.rate, v.detune, card);
|
|
grid.appendChild(card);
|
|
});
|
|
|
|
// Crop controls
|
|
document.getElementById('cropStart').oninput = updateCropDuration;
|
|
document.getElementById('cropEnd').oninput = updateCropDuration;
|
|
document.getElementById('previewCrop').onclick = () => playCropped(1.0, 0);
|
|
document.getElementById('playCropped').onclick = () => playCropped(variations[0].rate, variations[0].detune);
|
|
document.getElementById('copyCropCmd').onclick = copyCropCommand;
|
|
|
|
// Split controls
|
|
document.getElementById('uwEnd').oninput = updateSplitCommands;
|
|
document.getElementById('wuStart').oninput = updateSplitCommands;
|
|
document.getElementById('previewUw').onclick = () => playSplit('uw', 1.0, 0);
|
|
document.getElementById('previewWu').onclick = () => playSplit('wu', 1.0, 0);
|
|
document.getElementById('testUwVar').onclick = () => playSplit('uw', variations[0].rate, variations[0].detune);
|
|
document.getElementById('testWuVar').onclick = () => playSplit('wu', variations[0].rate, variations[0].detune);
|
|
document.getElementById('copyUwCmd').onclick = () => copyCommand(uwCommand, 'copyUwCmd');
|
|
document.getElementById('copyWuCmd').onclick = () => copyCommand(wuCommand, 'copyWuCmd');
|
|
|
|
// Main controls
|
|
document.getElementById('playAll').onclick = playAll;
|
|
document.getElementById('playSelected').onclick = playSelected;
|
|
document.getElementById('stop').onclick = stop;
|
|
document.getElementById('selectAll').onclick = () => {
|
|
variations.forEach((_, i) => selected.add(i));
|
|
document.querySelectorAll('.card').forEach(c => {
|
|
c.classList.add('selected');
|
|
c.querySelector('.checkbox').checked = true;
|
|
});
|
|
updateOutput();
|
|
};
|
|
document.getElementById('clearAll').onclick = () => {
|
|
selected.clear();
|
|
document.querySelectorAll('.card').forEach(c => {
|
|
c.classList.remove('selected');
|
|
c.querySelector('.checkbox').checked = false;
|
|
});
|
|
updateOutput();
|
|
};
|
|
|
|
load().then(() => {
|
|
// Set initial end time to actual duration
|
|
document.getElementById('cropEnd').value = buffer.duration.toFixed(2);
|
|
updateCropDuration();
|
|
|
|
// Initialize split commands if cropped file exists
|
|
if (croppedBuffer) {
|
|
updateSplitCommands();
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|