Drawaria Audio Player Plus

Sound Reproducer for drawaria (paste code in console to work).

// ==UserScript==
// @name         Drawaria Audio Player Plus
// @namespace    http://tampermonkey.net/
// @version      1.0
// @license MIT
// @description  Sound Reproducer for drawaria (paste code in console to work).
// @author       YouTubeDrawaria
// @match        https://drawaria.online/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=drawaria.online
// @grant        none // Se mantiene 'none' ya que no usamos funciones GM_ específicas.
// ==/UserScript==

(function() {
    'use strict';

    // --- Definición de los sonidos de Drawaria ---
    const drawariaSounds = [
        { name: "Guess", url: "https://drawaria.online/snd/guess.mp3" },
        { name: "Tick", url: "https://drawaria.online/snd/tick.mp3" },
        { name: "AFK", url: "https://drawaria.online/snd/afk.mp3" },
        { name: "Select Word", url: "https://drawaria.online/snd/selword.mp3" },
        { name: "Other Guess", url: "https://drawaria.online/snd/otherguess.mp3" },
        { name: "Turn Results", url: "https://drawaria.online/snd/turnresults.mp3" },
        { name: "Turn Aborted", url: "https://drawaria.online/snd/turnaborted.mp3" },
        { name: "Start Draw", url: "https://drawaria.online/snd/startdraw.mp3" }
    ];

    // --- HTML para el reproductor ---
    const playerHTML = `
        <div id="dap-audio-editor" class="dap-audio-editor">
            <div class="dap-header">
                <span class="dap-title">Drawaria Audio Player</span>
                <button id="dap-toggle-visibility" title="Mostrar/Ocultar Reproductor">🔽</button>
                <button id="dap-close-btn" title="Cerrar Reproductor">✕</button>
            </div>
            <div id="dap-main-content">
                <div class="dap-controls-panel">
                    <div class="dap-playback-controls">
                        <select id="dap-sound-selector">
                            ${drawariaSounds.map(sound => `<option value="${sound.url}">${sound.name}</option>`).join('')}
                        </select>
                        <button id="dap-play-pause-btn" title="Play/Pause">▶️</button>
                        <button id="dap-stop-btn" title="Stop">⏹️</button>
                    </div>
                    <div class="dap-time-display">
                        <span id="dap-current-time">00:00.000</span> / <span id="dap-total-duration">00:00.000</span>
                    </div>
                    <div class="dap-extra-controls">
                        <label for="dap-volume-slider">Vol:</label>
                        <input type="range" id="dap-volume-slider" min="0" max="1" step="0.01" value="0.8" title="Volumen">
                        <label for="dap-speed-slider">Vel:</label>
                        <input type="range" id="dap-speed-slider" min="0.25" max="3" step="0.05" value="1" title="Velocidad">
                    </div>
                </div>
                <div class="dap-waveform-section">
                    <div class="dap-track-info">
                        <span id="dap-track-name" class="dap-track-name">TRACK 1</span>
                        <label class="dap-switch">
                            ON
                            <input type="checkbox" checked disabled>
                            <span class="dap-slider-switch"></span>
                        </label>
                    </div>
                    <canvas id="dap-waveform-canvas"></canvas>
                </div>
            </div>
        </div>
    `;

    // --- CSS para el reproductor ---
    const playerCSS = `
        .dap-audio-editor {
            position: fixed;
            bottom: 10px;
            right: 10px;
            width: 450px;
            max-width: 90vw;
            background-color: #34495e;
            border-radius: 8px;
            padding: 10px;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
            color: #ecf0f1;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            font-size: 12px;
            z-index: 9999;
            overflow: hidden;
            transition: height 0.3s ease-out, width 0.3s ease-out, padding 0.3s ease-out;
            resize: both; /* Permite redimensionar con el ratón */
            min-width: 160px; /* Ancho mínimo para el estado oculto */
            min-height: 40px; /* Alto mínimo para el estado oculto */
        }
        .dap-audio-editor.dap-hidden {
            height: 40px; /* Altura solo para la barra de título */
            width: 160px; /* Ancho reducido cuando está oculto */
            padding: 5px;
            overflow: hidden;
        }
        .dap-audio-editor.dap-hidden #dap-main-content {
            display: none;
        }
        .dap-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding-bottom: 8px;
            border-bottom: 1px solid #2c3e50;
            margin-bottom: 10px;
            cursor: grab; /* Indica que se puede arrastrar */
        }
        .dap-header:active {
            cursor: grabbing; /* Cursor cuando se está arrastrando */
        }
        .dap-title {
            font-weight: bold;
            flex-grow: 1; /* Permite que ocupe el espacio restante */
            user-select: none; /* Evita selección de texto al arrastrar */
        }
        .dap-header button {
            background: #4a6178;
            border: none;
            color: white;
            padding: 3px 6px;
            border-radius: 4px;
            cursor: pointer;
            margin-left: 5px;
            flex-shrink: 0; /* Evita que los botones se encojan */
        }
        .dap-header button:hover {
            background-color: #5d7a99;
        }
        .dap-controls-panel {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 10px;
            flex-wrap: wrap;
            gap: 8px;
        }
        .dap-playback-controls, .dap-extra-controls {
            display: flex;
            align-items: center;
            gap: 8px;
        }
        #dap-sound-selector, .dap-playback-controls button, .dap-extra-controls input[type="range"] {
            padding: 6px 8px;
            background-color: #4a6178;
            border: 1px solid #5d7a99;
            color: #ecf0f1;
            border-radius: 4px;
            cursor: pointer;
            font-size: 0.9em;
            white-space: nowrap; /* Evita que los botones se rompan en varias líneas */
        }
        #dap-sound-selector { min-width: 100px; }
        .dap-playback-controls button:hover, #dap-sound-selector:hover { background-color: #5d7a99; }
        .dap-extra-controls input[type="range"] { accent-color: #3498db; max-width: 70px;}
        .dap-time-display {
            font-size: 1em;
            font-variant-numeric: tabular-nums;
            background-color: #222;
            padding: 4px 8px;
            border-radius: 3px;
            color: #fff;
            white-space: nowrap;
        }
        .dap-waveform-section {
            background-color: #283747;
            padding: 8px;
            border-radius: 4px;
        }
        .dap-track-info {
            display: flex;
            align-items: center;
            justify-content: space-between;
            margin-bottom: 8px;
            padding: 0 4px;
        }
        .dap-track-name { font-weight: bold; }
        .dap-switch {
            position: relative;
            display: inline-block;
            width: 50px;
            height: 20px;
            line-height: 20px;
            font-size: 0.8em;
        }
        .dap-switch input { opacity: 0; width: 0; height: 0; }
        .dap-switch .dap-slider-switch {
            position: absolute;
            cursor: pointer;
            top: 0;
            left: 25px;
            right: 0;
            bottom: 0;
            background-color: #ccc;
            transition: .4s;
            border-radius: 20px;
            width: 25px;
        }
        .dap-switch .dap-slider-switch:before {
            position: absolute;
            content: "";
            height: 14px;
            width: 14px;
            left: 3px;
            bottom: 3px;
            background-color: white;
            transition: .4s;
            border-radius: 50%;
        }
        .dap-switch input:checked + .dap-slider-switch { background-color: #2196F3; }
        .dap-switch input:checked + .dap-slider-switch:before { transform: translateX(5px); }
        #dap-waveform-canvas {
            width: 100%;
            height: 100px;
            background-color: #1f2b38;
            border-radius: 3px;
            border: 1px solid #4a6178;
            box-sizing: border-box; /* Asegura que padding y border no aumenten el tamaño */
        }
    `;

    // Función para inyectar CSS en el documento (ya que @grant none no permite GM_addStyle)
    function addStyle(css) {
        const style = document.createElement('style');
        style.type = 'text/css';
        style.appendChild(document.createTextNode(css));
        document.head.appendChild(style);
    }

    // --- Lógica JavaScript del Reproductor ---
    function initializePlayerLogic() {
        // Obtener el elemento raíz del reproductor, ya que ahora está en el DOM
        const audioEditorDiv = document.getElementById('dap-audio-editor');

        // Verificar si el elemento raíz se encontró. Si no, algo salió mal.
        if (!audioEditorDiv) {
            console.error("Drawaria Audio Player: El elemento raíz 'dap-audio-editor' no se encontró en el DOM. El reproductor no puede inicializarse.");
            return;
        }

        // Obtener elementos usando querySelector dentro del contenedor principal
        const soundSelector = audioEditorDiv.querySelector('#dap-sound-selector');
        const playPauseBtn = audioEditorDiv.querySelector('#dap-play-pause-btn');
        const stopBtn = audioEditorDiv.querySelector('#dap-stop-btn');
        const volumeSlider = audioEditorDiv.querySelector('#dap-volume-slider');
        const speedSlider = audioEditorDiv.querySelector('#dap-speed-slider');
        const currentTimeDisplay = audioEditorDiv.querySelector('#dap-current-time');
        const totalDurationDisplay = audioEditorDiv.querySelector('#dap-total-duration');
        const waveformCanvas = audioEditorDiv.querySelector('#dap-waveform-canvas');
        const trackNameDisplay = audioEditorDiv.querySelector('#dap-track-name');
        const toggleVisibilityBtn = audioEditorDiv.querySelector('#dap-toggle-visibility');
        const closeBtn = audioEditorDiv.querySelector('#dap-close-btn');
        const dapHeader = audioEditorDiv.querySelector('.dap-header'); // Para arrastrar

        // Segunda verificación, más detallada para los sub-elementos.
        // Si uno no se encuentra, hay un problema con la estructura HTML o el CSS.
        if (!soundSelector || !playPauseBtn || !stopBtn || !volumeSlider || !speedSlider || !currentTimeDisplay || !totalDurationDisplay || !waveformCanvas || !trackNameDisplay || !toggleVisibilityBtn || !closeBtn || !dapHeader) {
            console.error("Drawaria Audio Player: Uno o más elementos internos del reproductor no se encontraron. La estructura HTML podría estar incompleta o los selectores son incorrectos.");
            // Opcional: Eliminar el reproductor si está defectuoso
            audioEditorDiv.remove();
            return;
        }

        let canvasCtx = waveformCanvas.getContext('2d'); // Ahora podemos obtener el contexto del canvas
        let audioContext;
        let audioBuffer;
        let sourceNode;
        let gainNode;
        let isPlaying = false;
        let startTime = 0;
        let startOffset = 0;
        let animationFrameId;

        // Variables para hacer el reproductor draggable
        let isDragging = false;
        let offsetX, offsetY;

        function initAudioContext() {
            if (!audioContext) {
                audioContext = new (window.AudioContext || window.webkitAudioContext)();
                gainNode = audioContext.createGain();
                gainNode.connect(audioContext.destination);
                gainNode.gain.value = parseFloat(volumeSlider.value);
            }
        }

        async function loadSound(soundUrl, soundName) {
            initAudioContext();
            if (sourceNode) {
                try { sourceNode.stop(); } catch (e) { /* Ya podría estar detenido */ }
                sourceNode.disconnect();
                sourceNode = null;
            }
            isPlaying = false;
            playPauseBtn.innerHTML = '▶️';
            startOffset = 0;
            currentTimeDisplay.textContent = formatTime(0);
            if (animationFrameId) cancelAnimationFrame(animationFrameId);
            updateControlsState(false);
            trackNameDisplay.textContent = soundName || "LOADING...";

            try {
                const response = await fetch(soundUrl);
                if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
                const arrayBuffer = await response.arrayBuffer();
                audioContext.decodeAudioData(arrayBuffer, (buffer) => {
                    audioBuffer = buffer;
                    totalDurationDisplay.textContent = formatTime(audioBuffer.duration);
                    drawWaveform(audioBuffer);
                    updateControlsState(true);
                    trackNameDisplay.textContent = soundName || "TRACK 1";
                }, (error) => {
                    console.error('Drawaria Audio Player: Error decodificando datos de audio:', error);
                    totalDurationDisplay.textContent = 'Decode Err';
                    trackNameDisplay.textContent = "ERROR";
                    drawWaveform(null); // Limpiar el canvas en caso de error
                });
            } catch (error) {
                console.error('Drawaria Audio Player: Error obteniendo el audio:', error);
                totalDurationDisplay.textContent = 'Fetch Err';
                trackNameDisplay.textContent = "ERROR";
                drawWaveform(null); // Limpiar el canvas en caso de error
            }
        }

        function updateControlsState(enabled) {
            playPauseBtn.disabled = !enabled;
            stopBtn.disabled = !enabled;
            speedSlider.disabled = !enabled;
            // El slider de volumen puede estar siempre habilitado si gainNode existe
        }

        function togglePlayPause() {
            if (!audioBuffer || !audioContext) return;
            // Reanudar el AudioContext si está suspendido (necesario por políticas de navegador)
            if (audioContext.state === 'suspended') {
                audioContext.resume();
            }

            if (isPlaying) { // Si está reproduciendo -> Pausar
                if (sourceNode) try { sourceNode.stop(); } catch (e) {}
                startOffset += (audioContext.currentTime - startTime);
                isPlaying = false;
                playPauseBtn.innerHTML = '▶️';
                if (animationFrameId) cancelAnimationFrame(animationFrameId);
            } else { // Si está pausado o detenido -> Reproducir
                sourceNode = audioContext.createBufferSource();
                sourceNode.buffer = audioBuffer;
                sourceNode.playbackRate.value = parseFloat(speedSlider.value);
                sourceNode.connect(gainNode);
                startTime = audioContext.currentTime;
                sourceNode.start(0, startOffset % audioBuffer.duration);
                isPlaying = true;
                playPauseBtn.innerHTML = '⏸️';
                updateCurrentTime();
                sourceNode.onended = () => {
                    // Solo si terminó de forma natural, no por un stop/pause explícito
                    if (isPlaying && (audioContext.currentTime - startTime + startOffset) >= audioBuffer.duration - 0.05) {
                        isPlaying = false;
                        playPauseBtn.innerHTML = '▶️';
                        startOffset = 0;
                        currentTimeDisplay.textContent = formatTime(audioBuffer.duration);
                        if (animationFrameId) cancelAnimationFrame(animationFrameId);
                    }
                };
            }
        }

        function updateCurrentTime() {
            if (!isPlaying || !audioBuffer || !audioContext) return;
            let elapsedTime = (audioContext.currentTime - startTime) + startOffset;
            if (elapsedTime >= audioBuffer.duration) {
                elapsedTime = audioBuffer.duration;
            }
            currentTimeDisplay.textContent = formatTime(elapsedTime);
            animationFrameId = requestAnimationFrame(updateCurrentTime);
        }

        function stopSound() {
            if (sourceNode && isPlaying) {
                try { sourceNode.stop(); } catch (e) {}
            }
            isPlaying = false;
            playPauseBtn.innerHTML = '▶️';
            startOffset = 0;
            currentTimeDisplay.textContent = formatTime(0);
            if (animationFrameId) cancelAnimationFrame(animationFrameId);
        }

        volumeSlider.addEventListener('input', (e) => {
            if (gainNode) gainNode.gain.value = parseFloat(e.target.value);
        });

        speedSlider.addEventListener('input', (e) => {
            const newSpeed = parseFloat(e.target.value);
            if (sourceNode && isPlaying) sourceNode.playbackRate.value = newSpeed;
        });

        function formatTime(timeInSeconds) {
            const minutes = Math.floor(timeInSeconds / 60);
            const seconds = Math.floor(timeInSeconds % 60);
            const milliseconds = Math.floor((timeInSeconds % 1) * 1000);
            return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(milliseconds).padStart(3, '0')}`;
        }

        function drawWaveform(buffer) {
            // Asegurarse de que el canvas tenga las dimensiones correctas antes de dibujar
            const width = waveformCanvas.offsetWidth;
            const height = waveformCanvas.offsetHeight;

            // Solo redimensionar si es necesario para evitar flickering constante
            if (waveformCanvas.width !== width || waveformCanvas.height !== height) {
                waveformCanvas.width = width;
                waveformCanvas.height = height;
            }

            const amp = height / 2;

            canvasCtx.clearRect(0, 0, width, height);
            canvasCtx.fillStyle = '#1f2b38';
            canvasCtx.fillRect(0, 0, width, height);

            if (!buffer) return;

            const data = buffer.getChannelData(0); // Tomar el primer canal
            const step = Math.ceil(data.length / width);

            canvasCtx.lineWidth = 1;
            canvasCtx.strokeStyle = '#3498db'; // Azul claro
            canvasCtx.beginPath();

            for (let i = 0; i < width; i++) {
                let minVal = 1.0;
                let maxVal = -1.0;
                for (let j = 0; j < step; j++) {
                    const sampleIndex = (i * step) + j;
                    if (sampleIndex < data.length) {
                        const datum = data[sampleIndex];
                        if (datum < minVal) minVal = datum;
                        if (datum > maxVal) maxVal = datum;
                    }
                }
                canvasCtx.moveTo(i, amp - (Math.abs(maxVal) * amp));
                canvasCtx.lineTo(i, amp + (Math.abs(minVal) * amp));
            }
            canvasCtx.stroke();

            // Dibujar línea central roja
            canvasCtx.beginPath();
            canvasCtx.strokeStyle = 'rgba(200, 50, 50, 0.6)'; // Rojo semi-transparente
            canvasCtx.lineWidth = 1;
            canvasCtx.moveTo(0, amp);
            canvasCtx.lineTo(width, amp);
            canvasCtx.stroke();
        }

        soundSelector.addEventListener('change', (e) => {
            const selectedOption = e.target.options[e.target.selectedIndex];
            loadSound(selectedOption.value, selectedOption.textContent);
        });

        playPauseBtn.addEventListener('click', () => {
            if (!audioContext) initAudioContext();
            if (audioContext.state === 'suspended') {
                audioContext.resume().then(togglePlayPause);
            } else {
                togglePlayPause();
            }
        });
        stopBtn.addEventListener('click', stopSound);

        toggleVisibilityBtn.addEventListener('click', () => {
            audioEditorDiv.classList.toggle('dap-hidden');
            if (audioEditorDiv.classList.contains('dap-hidden')) {
                toggleVisibilityBtn.textContent = '🔼'; // Flecha hacia arriba para mostrar
                toggleVisibilityBtn.title = "Mostrar Reproductor";
            } else {
                toggleVisibilityBtn.textContent = '🔽'; // Flecha hacia abajo para ocultar
                toggleVisibilityBtn.title = "Ocultar Reproductor";
                // Redibujar waveform si estaba oculto o fue redimensionado
                if (audioBuffer) drawWaveform(audioBuffer);
            }
        });

        closeBtn.addEventListener('click', () => {
            // Detener cualquier reproducción antes de cerrar
            stopSound();
            // Cerrar el AudioContext para liberar recursos
            if (audioContext) {
                audioContext.close().then(() => {
                    audioContext = null;
                    gainNode = null;
                }).catch(e => console.error("Error al cerrar AudioContext:", e));
            }
            audioEditorDiv.remove(); // Eliminar el elemento del DOM
        });

        // Funcionalidad de arrastrar (draggable) por el encabezado
        dapHeader.addEventListener('mousedown', (e) => {
            // Solo arrastrar si el clic no es en un botón dentro del encabezado
            if (e.target.tagName !== 'BUTTON' && !e.target.closest('button')) {
                isDragging = true;
                // Guardar la posición inicial del puntero en relación al elemento
                offsetX = e.clientX - audioEditorDiv.getBoundingClientRect().left;
                offsetY = e.clientY - audioEditorDiv.getBoundingClientRect().top;
                audioEditorDiv.style.cursor = 'grabbing';
                e.preventDefault(); // Prevenir la selección de texto al arrastrar
            }
        });

        document.addEventListener('mousemove', (e) => {
            if (isDragging) {
                // Calcular las nuevas posiciones
                let newLeft = e.clientX - offsetX;
                let newTop = e.clientY - offsetY;

                // Limitar para que no salga demasiado de la ventana
                newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - audioEditorDiv.offsetWidth));
                newTop = Math.max(0, Math.min(newTop, window.innerHeight - audioEditorDiv.offsetHeight));

                audioEditorDiv.style.left = `${newLeft}px`;
                audioEditorDiv.style.top = `${newTop}px`;
                // Es importante desactivar 'right' y 'bottom' cuando se usa 'left' y 'top' para arrastrar
                audioEditorDiv.style.right = 'auto';
                audioEditorDiv.style.bottom = 'auto';
            }
        });

        document.addEventListener('mouseup', () => {
            if (isDragging) {
                isDragging = false;
                dapHeader.style.cursor = 'grab'; // Restablecer cursor del encabezado
            }
        });

        // Asegurar que el canvas tenga un tamaño inicial para dibujar
        // Esto es importante porque offsetWidth/offsetHeight pueden ser 0 si el elemento está oculto
        // Se puede redimensionar y dibujar una vez que se muestra o carga el audio
        waveformCanvas.width = waveformCanvas.offsetWidth;
        waveformCanvas.height = waveformCanvas.offsetHeight;


        // Carga inicial y estado
        updateControlsState(false);
        totalDurationDisplay.textContent = '00:00.000';
        currentTimeDisplay.textContent = '00:00.000';
        if (soundSelector.options.length > 0) {
            const firstSound = soundSelector.options[0];
            loadSound(firstSound.value, firstSound.textContent);
        } else {
            trackNameDisplay.textContent = "NO SOUNDS";
        }
        // El reproductor comienza *expandido* por defecto (quitamos .dap-hidden inicial)
        // Si quieres que comience oculto, quita esta línea y re-añade la clase y el icono de toggle
        // audioEditorDiv.classList.add('dap-hidden');
        // toggleVisibilityBtn.textContent = '🔼';
        // toggleVisibilityBtn.title = "Mostrar Reproductor";
        drawWaveform(null); // Dibujar el canvas vacío al inicio
    }

    // --- Inyección de UI y Ejecución ---
    function setupAndRun() {
        // Inyectar CSS
        addStyle(playerCSS);

        // Inyectar el HTML del reproductor directamente al body
        // Esto es más confiable que crear un div intermedio y luego transferir firstChild
        document.body.insertAdjacentHTML('beforeend', playerHTML);

        // Ya que el HTML se inserta como una cadena, necesitamos esperar un momento
        // para que el navegador lo parsee completamente y los elementos estén disponibles
        // en el DOM antes de intentar obtener sus referencias con getElementById/querySelector.
        setTimeout(initializePlayerLogic, 0); // Ejecutar la lógica de inicialización en el siguiente "tick" del navegador
    }

    // Esperar a que el DOM de la página original de Drawaria esté completamente cargado
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', setupAndRun);
    } else {
        setupAndRun(); // Si ya está cargado (ej. Tampermonkey se ejecuta tarde), ejecutar inmediatamente
    }
})();