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>
396 lines
15 KiB
HTML
396 lines
15 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 Pack 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: 0.5rem; }
|
|
.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); }
|
|
.section {
|
|
background: rgba(255,255,255,0.03); border: 2px solid rgba(255,255,255,0.1);
|
|
border-radius: 12px; padding: 1.5rem; margin-bottom: 2rem;
|
|
}
|
|
.section-title {
|
|
font-size: 1.3rem; font-weight: 600; margin-bottom: 1rem;
|
|
padding-bottom: 0.5rem; border-bottom: 2px solid rgba(255,255,255,0.2);
|
|
}
|
|
.grid {
|
|
display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: 1rem; margin-top: 1rem;
|
|
}
|
|
.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: 18px; height: 18px; cursor: pointer;
|
|
}
|
|
.index { font-size: 0.8rem; color: rgba(255,255,255,0.4); margin-bottom: 0.25rem; }
|
|
.transform { font-size: 0.85rem; color: rgba(255,255,255,0.7); font-weight: 600; }
|
|
.params { font-size: 0.7rem; 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 textarea {
|
|
width: 100%; background: rgba(0,0,0,0.5); padding: 1rem; border-radius: 4px;
|
|
overflow-x: auto; color: #fff; font-size: 0.85rem; font-family: monospace;
|
|
min-height: 200px; max-height: 400px; border: 2px solid rgba(255,255,255,0.2);
|
|
resize: vertical;
|
|
}
|
|
.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; }
|
|
.uwu { color: #ff69b4; }
|
|
.uw { color: #69b4ff; }
|
|
.wu { color: #b4ff69; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>🎀 UwU Sound Pack Selector 🎀</h1>
|
|
<div class="subtitle">60 variations from 3 base sounds - Select your favorites!</div>
|
|
<div class="info" id="infoBox">⏳ Loading audio files...</div>
|
|
|
|
<div class="stats">
|
|
<div class="stat">Total: <strong id="totalCount">60</strong></div>
|
|
<div class="stat">Selected: <strong id="selectedCount">0</strong></div>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<button id="playAll">▶ Play All (60)</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>
|
|
|
|
<!-- UwU Section -->
|
|
<div class="section">
|
|
<div class="section-title"><span class="uwu">🎵 "uwu" Variations</span> (20 from full sound)</div>
|
|
<div class="grid" id="gridUwu"></div>
|
|
</div>
|
|
|
|
<!-- Uw Section -->
|
|
<div class="section">
|
|
<div class="section-title"><span class="uw">🎵 "uw" Variations</span> (20 from first part)</div>
|
|
<div class="grid" id="gridUw"></div>
|
|
</div>
|
|
|
|
<!-- Wu Section -->
|
|
<div class="section">
|
|
<div class="section-title"><span class="wu">🎵 "wu" Variations</span> (20 from second part)</div>
|
|
<div class="grid" id="gridWu"></div>
|
|
</div>
|
|
|
|
<div class="output">
|
|
<h2>Selected Variations (Copy & Paste to Resume)</h2>
|
|
<textarea id="outputText" placeholder="// No variations selected yet. Click checkboxes above! // OR paste previous selection here to restore!">// No variations selected yet. Click checkboxes above!</textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Generate 20 variations for each base sound
|
|
const variationsUwu = [
|
|
{ rate: 0.8, detune: -200 }, { rate: 0.85, detune: -150 }, { rate: 0.9, detune: -100 },
|
|
{ rate: 0.95, detune: -50 }, { rate: 1.0, detune: 0 }, { rate: 1.0, detune: 50 },
|
|
{ rate: 1.05, detune: 100 }, { rate: 1.1, detune: 150 }, { rate: 1.15, detune: 200 },
|
|
{ rate: 1.2, detune: 250 }, { rate: 1.25, detune: 300 }, { rate: 1.3, detune: 350 },
|
|
{ rate: 1.35, detune: 400 }, { rate: 1.4, detune: 450 }, { rate: 0.75, detune: -250 },
|
|
{ rate: 0.9, detune: 0 }, { rate: 1.1, detune: 100 }, { rate: 1.2, detune: 200 },
|
|
{ rate: 1.3, detune: 300 }, { rate: 1.45, detune: 500 }
|
|
];
|
|
|
|
const variationsUw = [
|
|
{ rate: 0.8, detune: -150 }, { rate: 0.85, detune: -100 }, { rate: 0.9, detune: -50 },
|
|
{ rate: 0.95, detune: 0 }, { rate: 1.0, detune: 50 }, { rate: 1.05, detune: 100 },
|
|
{ rate: 1.1, detune: 150 }, { rate: 1.15, detune: 200 }, { rate: 1.2, detune: 250 },
|
|
{ rate: 1.25, detune: 300 }, { rate: 1.3, detune: 350 }, { rate: 1.35, detune: 400 },
|
|
{ rate: 1.4, detune: 450 }, { rate: 1.45, detune: 500 }, { rate: 0.75, detune: -200 },
|
|
{ rate: 0.85, detune: -50 }, { rate: 1.0, detune: 100 }, { rate: 1.15, detune: 250 },
|
|
{ rate: 1.25, detune: 350 }, { rate: 1.5, detune: 550 }
|
|
];
|
|
|
|
const variationsWu = [
|
|
{ rate: 0.8, detune: -100 }, { rate: 0.85, detune: -50 }, { rate: 0.9, detune: 0 },
|
|
{ rate: 0.95, detune: 50 }, { rate: 1.0, detune: 100 }, { rate: 1.05, detune: 150 },
|
|
{ rate: 1.1, detune: 200 }, { rate: 1.15, detune: 250 }, { rate: 1.2, detune: 300 },
|
|
{ rate: 1.25, detune: 350 }, { rate: 1.3, detune: 400 }, { rate: 1.35, detune: 450 },
|
|
{ rate: 1.4, detune: 500 }, { rate: 1.45, detune: 550 }, { rate: 0.7, detune: -150 },
|
|
{ rate: 0.8, detune: 0 }, { rate: 1.0, detune: 150 }, { rate: 1.2, detune: 300 },
|
|
{ rate: 1.3, detune: 450 }, { rate: 1.5, detune: 600 }
|
|
];
|
|
|
|
const allVariations = [
|
|
...variationsUwu.map((v, i) => ({ ...v, base: 'uwu', index: i })),
|
|
...variationsUw.map((v, i) => ({ ...v, base: 'uw', index: i })),
|
|
...variationsWu.map((v, i) => ({ ...v, base: 'wu', index: i }))
|
|
];
|
|
|
|
const ctx = new (window.AudioContext || window.webkitAudioContext)();
|
|
const gain = ctx.createGain();
|
|
gain.gain.value = 0.7;
|
|
gain.connect(ctx.destination);
|
|
|
|
const buffers = {};
|
|
let src = null;
|
|
let playing = false;
|
|
const selected = new Set();
|
|
|
|
async function load() {
|
|
const files = {
|
|
'uwu': '../assets/uwu/uwu-base-cropped.mp3',
|
|
'uw': '../assets/uwu/uwu-base-uw.mp3',
|
|
'wu': '../assets/uwu/uwu-base-wu.mp3'
|
|
};
|
|
|
|
for (const [key, path] of Object.entries(files)) {
|
|
const res = await fetch(path);
|
|
buffers[key] = await ctx.decodeAudioData(await res.arrayBuffer());
|
|
}
|
|
}
|
|
|
|
function play(base, rate, detune, card) {
|
|
console.log(`Playing: base=${base}, rate=${rate}, detune=${detune}`);
|
|
|
|
if (!buffers[base]) {
|
|
console.error(`Buffer not loaded for: ${base}`);
|
|
alert(`Audio not loaded yet for "${base}". Please wait...`);
|
|
return;
|
|
}
|
|
|
|
if (src) {
|
|
try { src.stop(); } catch (e) {}
|
|
}
|
|
document.querySelectorAll('.card').forEach(c => c.classList.remove('playing'));
|
|
|
|
if (ctx.state === 'suspended') {
|
|
console.log('Resuming suspended AudioContext...');
|
|
ctx.resume();
|
|
}
|
|
|
|
src = ctx.createBufferSource();
|
|
src.buffer = buffers[base];
|
|
src.playbackRate.value = rate;
|
|
src.detune.value = detune;
|
|
src.connect(gain);
|
|
if (card) card.classList.add('playing');
|
|
src.onended = () => {
|
|
console.log('Sound ended');
|
|
if (card) card.classList.remove('playing');
|
|
};
|
|
src.start();
|
|
console.log('Sound started');
|
|
}
|
|
|
|
async function playAll() {
|
|
playing = true;
|
|
for (let i = 0; i < allVariations.length && playing; i++) {
|
|
const v = allVariations[i];
|
|
const card = document.querySelector(`[data-i="${i}"]`);
|
|
play(v.base, v.rate, v.detune, card);
|
|
await new Promise(r => setTimeout(r, (buffers[v.base].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 = allVariations[i];
|
|
const card = document.querySelector(`[data-i="${i}"]`);
|
|
play(v.base, v.rate, v.detune, card);
|
|
await new Promise(r => setTimeout(r, (buffers[v.base].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'));
|
|
}
|
|
|
|
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').value = '// 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 = allVariations[i];
|
|
const semitones = (v.detune / 100).toFixed(1);
|
|
return `{ base: '${v.base}', rate: ${v.rate}, detune: ${v.detune} }, // #${i + 1}: "${v.base}" ${semitones > 0 ? '+' : ''}${semitones} semitones, ${v.rate}x speed`;
|
|
});
|
|
|
|
document.getElementById('outputText').value = lines.join('\n');
|
|
}
|
|
|
|
function restoreFromPaste() {
|
|
const text = document.getElementById('outputText').value;
|
|
const lines = text.split('\n').filter(l => l.trim() && !l.trim().startsWith('//'));
|
|
|
|
// Parse lines to find variation indices
|
|
const indices = [];
|
|
lines.forEach(line => {
|
|
const match = line.match(/#(\d+):/);
|
|
if (match) {
|
|
const index = parseInt(match[1]) - 1; // Convert to 0-based
|
|
if (index >= 0 && index < allVariations.length) {
|
|
indices.push(index);
|
|
}
|
|
}
|
|
});
|
|
|
|
if (indices.length > 0) {
|
|
// Clear and restore selections
|
|
selected.clear();
|
|
indices.forEach(i => selected.add(i));
|
|
|
|
// Update UI
|
|
document.querySelectorAll('.card').forEach(c => {
|
|
const cardIndex = parseInt(c.dataset.i);
|
|
const isSelected = selected.has(cardIndex);
|
|
c.classList.toggle('selected', isSelected);
|
|
c.querySelector('.checkbox').checked = isSelected;
|
|
});
|
|
|
|
updateOutput();
|
|
console.log(`Restored ${indices.length} selections from paste`);
|
|
}
|
|
}
|
|
|
|
function buildGrid(gridId, startIndex, count) {
|
|
const grid = document.getElementById(gridId);
|
|
for (let i = 0; i < count; i++) {
|
|
const globalIndex = startIndex + i;
|
|
const v = allVariations[globalIndex];
|
|
const card = document.createElement('div');
|
|
card.className = 'card';
|
|
card.dataset.i = globalIndex;
|
|
|
|
const checkbox = document.createElement('input');
|
|
checkbox.type = 'checkbox';
|
|
checkbox.className = 'checkbox';
|
|
checkbox.onclick = (e) => {
|
|
e.stopPropagation();
|
|
toggleSelect(globalIndex);
|
|
card.classList.toggle('selected');
|
|
};
|
|
|
|
const index = document.createElement('div');
|
|
index.className = 'index';
|
|
index.textContent = `#${globalIndex + 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`;
|
|
|
|
card.append(checkbox, index, transform, params);
|
|
card.onclick = () => play(v.base, v.rate, v.detune, card);
|
|
grid.appendChild(card);
|
|
}
|
|
}
|
|
|
|
document.getElementById('playAll').onclick = playAll;
|
|
document.getElementById('playSelected').onclick = playSelected;
|
|
document.getElementById('stop').onclick = stop;
|
|
document.getElementById('selectAll').onclick = () => {
|
|
allVariations.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();
|
|
};
|
|
|
|
// Detect paste to restore selections
|
|
document.getElementById('outputText').addEventListener('paste', (e) => {
|
|
setTimeout(restoreFromPaste, 100); // Delay to let paste complete
|
|
});
|
|
|
|
load().then(() => {
|
|
console.log('All audio files loaded successfully');
|
|
document.getElementById('infoBox').textContent = '🎵 Click cards to play. Check boxes to select. Copy list at bottom. Paste to restore!';
|
|
buildGrid('gridUwu', 0, 20);
|
|
buildGrid('gridUw', 20, 20);
|
|
buildGrid('gridWu', 40, 20);
|
|
}).catch(err => {
|
|
console.error('Failed to load audio:', err);
|
|
document.getElementById('infoBox').textContent = '❌ Failed to load audio files. Check console for details.';
|
|
document.getElementById('infoBox').style.background = 'rgba(255,100,100,0.2)';
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|