Botões de velocidade para o YouTube

Botões para controlar a velocidade de reprodução no YouTube, com menu suspenso e suporte multilíngue.

// ==UserScript==
// @name         Botões de velocidade para o YouTube
// @namespace    https://greasyfork.org/pt-BR/scripts/543571
// @version      2.2.6
// @description  Botões para controlar a velocidade de reprodução no YouTube, com menu suspenso e suporte multilíngue.
// @author       Ramon Machado
// @match        *://*.youtube.com/*
// @grant        none
// @license      GNU GPLv3
// @icon         https://cdn-icons-png.flaticon.com/512/4399/4399641.png
// ==/UserScript==

(function () {
    'use strict';

    // Traduções
    const texts = {
        pt: {
            increase: '▲ +Vel',
            reset: '■ 1x',
            decrease: '▼ -Vel',
            speedMsg: v => `Velocidade: ${v.toFixed(2)}x`
        },
        en: {
            increase: '▲ +Speed',
            reset: '■ 1x',
            decrease: '▼ -Speed',
            speedMsg: v => `Speed: ${v.toFixed(2)}x`
        },
        es: {
            increase: '▲ +Velocidad',
            reset: '■ 1x',
            decrease: '▼ -Velocidad',
            speedMsg: v => `Velocidad: ${v.toFixed(2)}x`
        },
        fr: {
            increase: '▲ +Vitesse',
            reset: '■ 1x',
            decrease: '▼ -Vitesse',
            speedMsg: v => `Vitesse : ${v.toFixed(2)}x`
        },
        de: {
            increase: '▲ +Geschw.',
            reset: '■ 1x',
            decrease: '▼ -Geschw.',
            speedMsg: v => `Geschwindigkeit: ${v.toFixed(2)}x`
        },
        it: {
            increase: '▲ +Velocità',
            reset: '■ 1x',
            decrease: '▼ -Velocità',
            speedMsg: v => `Velocità: ${v.toFixed(2)}x`
        },
        ja: {
            increase: '▲ 速く',
            reset: '■ 1x',
            decrease: '▼ 遅く',
            speedMsg: v => `速度: ${v.toFixed(2)}x`
        }
    };

    const lang = (navigator.language || 'pt').slice(0, 2);
    const t = texts[lang] || texts.pt;

    const styles = {
        button: {
            backgroundColor: 'rgba(0,0,0,0.7)',
            color: 'white',
            border: 'none',
            padding: '10px',
            borderRadius: '5px',
            cursor: 'pointer',
            fontSize: '16px',
            minWidth: '80px',
            width: '100%',
            textAlign: 'center',
            boxSizing: 'border-box'
        },
        dropdown: {
            position: 'absolute',
            right: '100%',
            bottom: '0',
            backgroundColor: 'rgba(0, 0, 0, 0.9)',
            padding: '5px',
            borderRadius: '5px',
            display: 'none',
            flexDirection: 'column',
            gap: '3px',
            zIndex: '9999'
        },
        option: {
            backgroundColor: '#222',
            color: 'white',
            padding: '5px 10px',
            border: 'none',
            cursor: 'pointer',
            fontSize: '14px',
            whiteSpace: 'nowrap'
        }
    };

    function applyStyle(el, styleObj) {
        Object.assign(el.style, styleObj);
    }

    function createButton(text, onClick) {
        const btn = document.createElement('button');
        btn.textContent = text;
        applyStyle(btn, styles.button);
        btn.addEventListener('click', onClick);
        return btn;
    }

    function showMessage(text) {
        let msg = document.getElementById('speed-message');
        if (!msg) {
            msg = document.createElement('div');
            msg.id = 'speed-message';
            Object.assign(msg.style, {
                position: 'fixed',
                bottom: '270px',
                right: '20px',
                backgroundColor: 'rgba(0,0,0,0.8)',
                color: 'white',
                padding: '5px 10px',
                borderRadius: '5px',
                fontSize: '16px',
                textAlign: 'center',
                pointerEvents: 'none',
                zIndex: '9999',
                display: 'none'
            });
            document.body.appendChild(msg);
        }
        msg.textContent = text;
        msg.style.display = 'block';
        clearTimeout(msg.timer);
        msg.timer = setTimeout(() => { msg.style.display = 'none'; }, 1500);
    }

    function waitForVideo(callback) {
        const checkVideo = () => {
            const video = document.querySelector('video');
            if (video) {
                callback(video);
            } else {
                setTimeout(checkVideo, 100);
            }
        };
        checkVideo();
    }

    function adjustSpeed(delta) {
        waitForVideo(video => {
            video.playbackRate = delta === 0 ? 1.0 : Math.min(Math.max(video.playbackRate + delta, 0.25), 16);
            showMessage(t.speedMsg(video.playbackRate));
        });
    }

    function setSpeed(value) {
        waitForVideo(video => {
            video.playbackRate = value;
            showMessage(t.speedMsg(value));
        });
    }

    function addSpeedControls() {
        if (!(location.pathname.startsWith('/watch') || location.pathname.startsWith('/shorts'))) {
            removeSpeedControls();
            return;
        }
        if (document.getElementById('speed-controls')) return;

        const container = document.createElement('div');
        container.id = 'speed-controls';
        Object.assign(container.style, {
            position: 'fixed',
            bottom: '140px',
            right: '20px',
            display: 'flex',
            flexDirection: 'column',
            gap: '5px',
            zIndex: '9999'
        });

        const increaseBtn = createButton(t.increase, () => adjustSpeed(0.25));
        const decreaseBtn = createButton(t.decrease, () => adjustSpeed(-0.25));

        const resetWrapper = document.createElement('div');
        resetWrapper.style.position = 'relative';

        const resetBtn = createButton(t.reset, () => adjustSpeed(0));

        const dropdown = document.createElement('div');
        applyStyle(dropdown, styles.dropdown);

        const speeds = [3.0, 2.0, 1.5, 1.25, 1.0, 0.75, 0.5];
        speeds.forEach(spd => {
            const opt = document.createElement('button');
            opt.textContent = `${spd}x`;
            applyStyle(opt, styles.option);
            opt.addEventListener('mouseenter', () => opt.style.backgroundColor = '#444');
            opt.addEventListener('mouseleave', () => opt.style.backgroundColor = '#222');
            opt.addEventListener('click', e => {
                e.stopPropagation();
                setSpeed(spd);
                dropdown.style.display = 'none';
            });
            dropdown.appendChild(opt);
        });

        resetWrapper.appendChild(resetBtn);
        resetWrapper.appendChild(dropdown);

        resetWrapper.addEventListener('mouseenter', () => {
            dropdown.style.display = 'flex';
        });
        resetWrapper.addEventListener('mouseleave', () => {
            dropdown.style.display = 'none';
        });

        container.appendChild(increaseBtn);
        container.appendChild(resetWrapper);
        container.appendChild(decreaseBtn);

        document.body.appendChild(container);
    }

    function removeSpeedControls() {
        const container = document.getElementById('speed-controls');
        const msg = document.getElementById('speed-message');
        if (container) container.remove();
        if (msg) msg.remove();
    }

    const observer = new MutationObserver(() => addSpeedControls());
    observer.observe(document.body, { childList: true, subtree: true });

    window.addEventListener('load', addSpeedControls);

    window.addEventListener('keydown', e => {
        if (['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName)) return;
        if (e.key === '+') adjustSpeed(0.25);
        if (e.key === ']') adjustSpeed(0.25);
        if (e.key === '-') adjustSpeed(-0.25);
        if (e.key === '[') adjustSpeed(-0.25);
        if (e.key === '=') adjustSpeed(0);
    });
})();