Botão Duplo de Velocidade/Qualidade para o YouTube.

Exibe dois botões, um deles é um ícone de Cronômetro para a Velocidade e o outro é uma Engrenagem para a Qualidade de Vídeo. A Qualidade e Velocidade escolhidas são salvas e usadas automaticamente nos próximos vídeos.

// ==UserScript==
// @name         Botão Duplo de Velocidade/Qualidade para o YouTube.
// @namespace    https://greasyfork.org/pt-BR/scripts/543571-modified
// @version      3.2.3
// @description  Exibe dois botões, um deles é um ícone de Cronômetro para a Velocidade e o outro é uma Engrenagem para a Qualidade de Vídeo. A Qualidade e Velocidade escolhidas são salvas e usadas automaticamente nos próximos vídeos.
// @author       matheus felipen
// @match        *://*.youtube.com/*
// @grant        none
// @license      GNU GPLv3
// @icon         https://i.ibb.co/WNzVD92X/cronometro.png
// ==/UserScript==

(function () {
    'use strict';

    const texts = {
        pt: {
            speedLabel: '⏱️ Velocidade',
            speedMsg: v => `Velocidade: ${v.toFixed(2)}x`,
            quality: '⚙️ Qualidade',
            qualityMsg: q => `Qualidade: ${q}`
        },
        en: {
            speedLabel: '⏱️ Speed',
            speedMsg: v => `Speed: ${v.toFixed(2)}x`,
            quality: '⚙️ Quality',
            qualityMsg: q => `Quality: ${q}`
        }
    };

    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: '85px', 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('custom-yt-message');
        if (!msg) {
            msg = document.createElement('div');
            msg.id = 'custom-yt-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 isAdPlaying() {
        return !!document.querySelector('.ad-showing');
    }

    function waitForVideo(callback) {
        const interval = setInterval(() => {
            const video = document.querySelector('video');
            if (video) {
                clearInterval(interval);
                callback(video);

                const observer = new MutationObserver(() => {
                    const savedSpeed = parseFloat(localStorage.getItem('youtubePlaybackSpeed')) || 1.0;
                    if (!isAdPlaying()) {
                        video.playbackRate = savedSpeed;
                    }
                });
                observer.observe(video, { attributes: true, attributeFilter: ['src'] });
            }
        }, 100);
    }

    function adjustSpeed(delta) {
        waitForVideo(video => {
            if (isAdPlaying()) return;
            const newRate = delta === 0 ? 1.0 : Math.min(Math.max(video.playbackRate + delta, 0.25), 16);
            video.playbackRate = newRate;
            localStorage.setItem('youtubePlaybackSpeed', newRate);
            showMessage(t.speedMsg(newRate));
        });
    }

    function setSpeed(value) {
        waitForVideo(video => {
            if (isAdPlaying()) return;
            video.playbackRate = value;
            localStorage.setItem('youtubePlaybackSpeed', value);
            showMessage(t.speedMsg(value));
        });
    }

    function getSavedQuality() { return localStorage.getItem('youtubeQuality') || 'Auto'; }
    function saveQuality(quality) { localStorage.setItem('youtubeQuality', quality); }
    function setVideoQuality(qualityLabel) {
        if (qualityLabel === 'Auto') {
            saveQuality('Auto');
            showMessage(t.qualityMsg('Automática'));
            return;
        }
        waitForVideo(() => {
            if (isAdPlaying()) return;
            const settingsButton = document.querySelector('.ytp-settings-button');
            if (!settingsButton) return;
            settingsButton.click();
            setTimeout(() => {
                const menuItems = document.querySelectorAll('.ytp-menuitem');
                const qualityMenuItem = Array.from(menuItems).find(item => (item.textContent || '').includes('Qualidade') || (item.textContent || '').includes('Quality'));
                if (!qualityMenuItem) {
                    settingsButton.click();
                    return;
                }
                qualityMenuItem.click();
                setTimeout(() => {
                    const qualityOptions = document.querySelectorAll('.ytp-quality-menu .ytp-menuitem');
                    const targetQualityOption = Array.from(qualityOptions).find(item => (item.textContent || '').startsWith(qualityLabel));
                    if (targetQualityOption) {
                        targetQualityOption.click();
                        saveQuality(qualityLabel);
                        showMessage(t.qualityMsg(qualityLabel));
                    } else {
                        settingsButton.click();
                    }
                }, 300);
            }, 300);
        });
    }

    function applySpeedAfterAd(speed) {
        const interval = setInterval(() => {
            const video = document.querySelector('video');
            if (video && !isAdPlaying()) {
                clearInterval(interval);
                video.playbackRate = speed;
            }
        }, 500);
    }

    function applyQualityAfterAd(quality) {
        const interval = setInterval(() => {
            if (!isAdPlaying()) {
                clearInterval(interval);
                setVideoQuality(quality);
            }
        }, 500);
    }

    function addControls() {
        if (document.getElementById('controls-container')) return;

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

        const qualityWrapper = document.createElement('div');
        qualityWrapper.style.position = 'relative';
        const qualityBtn = createButton(t.quality, () => {});
        const qualityDropdown = document.createElement('div');
        applyStyle(qualityDropdown, styles.dropdown);
        const qualityOptions = ['Auto', '144p', '240p', '360p', '480p', '720p', '1080p', '1440p', '2160p'];
        qualityOptions.forEach(q => {
            const opt = createButton(q, e => {
                e.stopPropagation();
                setVideoQuality(q);
                qualityDropdown.style.display = 'none';
            });
            applyStyle(opt, styles.option);
            opt.addEventListener('mouseenter', () => opt.style.backgroundColor = '#444');
            opt.addEventListener('mouseleave', () => opt.style.backgroundColor = '#222');
            qualityDropdown.appendChild(opt);
        });
        qualityWrapper.appendChild(qualityBtn);
        qualityWrapper.appendChild(qualityDropdown);
        qualityWrapper.addEventListener('mouseenter', () => qualityDropdown.style.display = 'flex');
        qualityWrapper.addEventListener('mouseleave', () => qualityDropdown.style.display = 'none');

        const speedWrapper = document.createElement('div');
        speedWrapper.style.position = 'relative';
        const speedBtn = createButton(t.speedLabel, () => adjustSpeed(0));
        const speedDropdown = document.createElement('div');
        applyStyle(speedDropdown, styles.dropdown);
        const speeds = [3.0, 2.0, 1.5, 1.25, 1.0, 0.75, 0.5];
        speeds.forEach(spd => {
            const opt = createButton(`${spd}x`, e => {
                e.stopPropagation();
                setSpeed(spd);
                speedDropdown.style.display = 'none';
            });
            applyStyle(opt, styles.option);
            opt.addEventListener('mouseenter', () => opt.style.backgroundColor = '#444');
            opt.addEventListener('mouseleave', () => opt.style.backgroundColor = '#222');
            speedDropdown.appendChild(opt);
        });

        speedWrapper.appendChild(speedBtn);
        speedWrapper.appendChild(speedDropdown);
        speedWrapper.addEventListener('mouseenter', () => speedDropdown.style.display = 'flex');
        speedWrapper.addEventListener('mouseleave', () => speedDropdown.style.display = 'none');

        container.appendChild(qualityWrapper);
        container.appendChild(speedWrapper);
        document.body.appendChild(container);

        function updateVisibility() {
            const controls = document.getElementById('controls-container');
            if (controls) {
                controls.style.display = document.fullscreenElement ? 'none' : 'flex';
            }
        }
        updateVisibility();
        document.addEventListener('fullscreenchange', updateVisibility);
    }

    function removeControls() {
        const container = document.getElementById('controls-container');
        if (container) container.remove();
    }

    function handlePageChange() {
        setTimeout(() => {
            const isVideoPage = location.pathname.startsWith('/watch') || location.pathname.startsWith('/shorts');
            if (isVideoPage) {
                addControls();
                waitForVideo(video => {});
                const savedQuality = getSavedQuality();
                if (savedQuality && savedQuality !== 'Auto') {
                    applyQualityAfterAd(savedQuality);
                }
                const savedSpeed = parseFloat(localStorage.getItem('youtubePlaybackSpeed')) || 1.0;
                applySpeedAfterAd(savedSpeed);
            } else {
                removeControls();
            }
        }, 500);
    }

    document.addEventListener('yt-navigate-finish', handlePageChange);
    if (document.body) { handlePageChange(); }
    else { document.addEventListener('DOMContentLoaded', handlePageChange); }

    window.addEventListener('keydown', e => {
        if (['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName) || document.activeElement.isContentEditable) return;
        const keyMap = { '+': 0.25, ']': 0.25, '-': -0.25, '[': -0.25 };
        if (keyMap[e.key] !== undefined) { adjustSpeed(keyMap[e.key]); }
        else if (e.key === '=') { adjustSpeed(0); }
    });
})();