314 lines
10 KiB
HTML
314 lines
10 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="theme-color" content="#4A90E2">
|
|
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
|
<meta name="theme-color" content="#121212" media="(prefers-color-scheme: dark)">
|
|
<link rel="manifest" href="manifest.json">
|
|
<title>Audio Cutter PWA</title>
|
|
<script src="wavesurfer.min.js"></script>
|
|
<script src="plugins/regions.min.js"></script>
|
|
<script>
|
|
if ('serviceWorker' in navigator) {
|
|
navigator.serviceWorker.register('./sw.js')
|
|
.then(reg => console.log('Registro exitoso', reg))
|
|
.catch(err => console.warn('Error al registrar', err));
|
|
}
|
|
|
|
if ('serviceWorker' in navigator) {
|
|
navigator.serviceWorker.register('./sw.js').then(reg => {
|
|
reg.onupdatefound = () => {
|
|
const installingWorker = reg.installing;
|
|
installingWorker.onstatechange = () => {
|
|
if (installingWorker.state === 'installed') {
|
|
if (navigator.serviceWorker.controller) {
|
|
// Aquí es donde le avisas al usuario
|
|
alert('Nueva versión disponible. Por favor, recarga la página.');
|
|
}
|
|
}
|
|
};
|
|
};
|
|
});
|
|
}
|
|
|
|
if ('serviceWorker' in navigator) {
|
|
window.addEventListener('load', () => {
|
|
navigator.serviceWorker.register('./sw.js').then(reg => {
|
|
|
|
// Detecta si hay una actualización esperando
|
|
reg.addEventListener('updatefound', () => {
|
|
const newWorker = reg.installing;
|
|
|
|
newWorker.addEventListener('statechange', () => {
|
|
// Cuando el nuevo SW se ha instalado completamente
|
|
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
|
|
|
// Crea una confirmación para el usuario
|
|
const r = confirm("¡Hay una nueva versión disponible! ¿Quieres actualizar ahora?");
|
|
if (r === true) {
|
|
window.location.reload(); // Recarga la página para aplicar cambios
|
|
}
|
|
|
|
}
|
|
});
|
|
});
|
|
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
</script>
|
|
<style>
|
|
:root {
|
|
--bg: #ffffff;
|
|
--card: #1e293b;
|
|
--accent: #3b82f6;
|
|
--texto: #f8f7f7;
|
|
}
|
|
|
|
/* Colores para Tema Oscuro */
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--fondo: #121212;
|
|
--texto: #0c0c0c;
|
|
--primario: #1a73e8;
|
|
--card: #d6dbe4;
|
|
}
|
|
}
|
|
|
|
/* Uso de las variables */
|
|
body {
|
|
background-color: var(--fondo);
|
|
color: var(--texto);
|
|
}
|
|
|
|
body {
|
|
font-family: sans-serif;
|
|
background: var(--bg);
|
|
/*color: white;*/
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 20px;
|
|
}
|
|
|
|
.card {
|
|
background: var(--card);
|
|
padding: 20px;
|
|
border-radius: 15px;
|
|
width: 100%;
|
|
max-width: 700px;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
#waveform {
|
|
background: #000;
|
|
border-radius: 10px;
|
|
margin: 20px 0;
|
|
border: 1px solid #334155;
|
|
}
|
|
|
|
.info {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-family: monospace;
|
|
color: #94a3b8;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.controls {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
}
|
|
|
|
button {
|
|
padding: 10px 16px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-weight: bold;
|
|
transition: 0.2s;
|
|
}
|
|
|
|
.btn-play {
|
|
background: #10b981;
|
|
color: white;
|
|
}
|
|
|
|
.btn-stop {
|
|
background: #ef4444;
|
|
color: white;
|
|
}
|
|
|
|
.btn-export {
|
|
background: var(--accent);
|
|
color: white;
|
|
}
|
|
|
|
.btn-zoom {
|
|
background: #475569;
|
|
color: white;
|
|
}
|
|
|
|
.btn-share {
|
|
background: #f59e0b;
|
|
color: white;
|
|
display: none;
|
|
}
|
|
|
|
#status {
|
|
font-size: 0.8rem;
|
|
margin-top: 10px;
|
|
text-align: center;
|
|
color: #64748b;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div class="card">
|
|
<h3>Audio Cutter PWA</h3>
|
|
<p>Cortador simple de audio multiplataforma gracias a PWA. Ingresa el archivo. Selecciona en la linea del tiempo y exporta tu fragmento.</p>
|
|
<input type="file" id="audioInput" accept="audio/*">
|
|
|
|
<div id="waveform"></div>
|
|
|
|
<div class="info">
|
|
<span id="totalTime">Total: 00:00</span>
|
|
<span id="selectionTime">Selección: 00:00 - 00:00</span>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<button class="btn-zoom" onclick="ws.zoom(ws.options.minPxPerSec + 30)">Zoom +</button>
|
|
<button class="btn-zoom" onclick="ws.zoom(ws.options.minPxPerSec - 30)">Zoom -</button>
|
|
<button class="btn-play" id="btnPlay">Probar</button>
|
|
<button class="btn-stop" id="btnStop">Parar</button>
|
|
<button class="btn-export" id="btnExport">Exportar</button>
|
|
<button class="btn-share" id="btnShare">Compartir</button>
|
|
</div>
|
|
<p id="status">Listo.</p>
|
|
</div>
|
|
|
|
<script>
|
|
let ws, wsRegions, activeRegion, lastBlob, originalFileType;
|
|
const audioInput = document.getElementById('audioInput');
|
|
const status = document.getElementById('status');
|
|
|
|
ws = WaveSurfer.create({
|
|
container: '#waveform',
|
|
waveColor: '#4f46e5',
|
|
progressColor: '#818cf8',
|
|
height: 120
|
|
});
|
|
|
|
wsRegions = ws.registerPlugin(WaveSurfer.Regions.create());
|
|
|
|
audioInput.onchange = (e) => {
|
|
const file = e.target.files[0];
|
|
if (file) {
|
|
originalFileType = file.type; // Guardamos el formato original
|
|
ws.load(URL.createObjectURL(file));
|
|
document.getElementById('btnShare').style.display = 'none';
|
|
}
|
|
};
|
|
|
|
ws.on('ready', () => {
|
|
const duration = ws.getDuration();
|
|
document.getElementById('totalTime').innerText = `Total: ${formatTime(duration)}`;
|
|
wsRegions.clearRegions();
|
|
activeRegion = wsRegions.addRegion({
|
|
start: 0,
|
|
end: Math.min(duration, 10),
|
|
color: 'rgba(59, 130, 246, 0.3)',
|
|
drag: true, resize: true
|
|
});
|
|
updateSelectionLabel();
|
|
});
|
|
|
|
wsRegions.on('region-updated', updateSelectionLabel);
|
|
|
|
function updateSelectionLabel() {
|
|
if (activeRegion) {
|
|
document.getElementById('selectionTime').innerText =
|
|
`Selección: ${formatTime(activeRegion.start)} - ${formatTime(activeRegion.end)}`;
|
|
}
|
|
}
|
|
|
|
function formatTime(s) {
|
|
return new Date(s * 1000).toISOString().substr(14, 5);
|
|
}
|
|
|
|
document.getElementById('btnPlay').onclick = () => activeRegion && activeRegion.play();
|
|
document.getElementById('btnStop').onclick = () => ws.stop();
|
|
|
|
// EXPORTACIÓN CON MEDIARECORDER (Mantiene formato de origen/comprimido)
|
|
document.getElementById('btnExport').onclick = async () => {
|
|
if (!activeRegion) return;
|
|
status.innerText = "Procesando...";
|
|
|
|
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
const sourceBuffer = ws.getDecodedData();
|
|
const duration = activeRegion.end - activeRegion.start;
|
|
|
|
const offlineCtx = new OfflineAudioContext(
|
|
sourceBuffer.numberOfChannels,
|
|
duration * sourceBuffer.sampleRate,
|
|
sourceBuffer.sampleRate
|
|
);
|
|
|
|
const source = offlineCtx.createBufferSource();
|
|
source.buffer = sourceBuffer;
|
|
source.connect(offlineCtx.destination);
|
|
source.start(0, activeRegion.start, duration);
|
|
|
|
const renderedBuffer = await offlineCtx.startRendering();
|
|
|
|
// Grabación del Stream para comprimir
|
|
const destination = audioCtx.createMediaStreamDestination();
|
|
const recorder = new MediaRecorder(destination.stream);
|
|
const chunks = [];
|
|
|
|
recorder.ondataavailable = (e) => chunks.push(e.data);
|
|
recorder.onstop = () => {
|
|
lastBlob = new Blob(chunks, { type: originalFileType || 'audio/mp4' });
|
|
const url = URL.createObjectURL(lastBlob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `cut_${audioInput.files[0].name}`;
|
|
a.click();
|
|
status.innerText = "¡Exportado!";
|
|
document.getElementById('btnShare').style.display = 'inline-block';
|
|
};
|
|
|
|
const playSource = audioCtx.createBufferSource();
|
|
playSource.buffer = renderedBuffer;
|
|
playSource.connect(destination);
|
|
|
|
recorder.start();
|
|
playSource.start();
|
|
playSource.onended = () => recorder.stop();
|
|
};
|
|
|
|
// COMPARTIR (Botón 3 bolitas nativo Android)
|
|
document.getElementById('btnShare').onclick = async () => {
|
|
if (!lastBlob) return;
|
|
const file = new File([lastBlob], `cut_${audioInput.files[0].name}`, { type: lastBlob.type });
|
|
if (navigator.canShare && navigator.canShare({ files: [file] })) {
|
|
await navigator.share({
|
|
files: [file],
|
|
title: 'Audio Cortado',
|
|
text: 'Compartido desde Audio Cutter PWA'
|
|
});
|
|
}
|
|
};
|
|
</script>
|
|
</body>
|
|
|
|
</html> |