YouTube Playback Speed Control++

Adds an immersive playback speed control interface to YouTube's bottom controls to extend the speed options, while also hiding the default one. Includes keyboard shortcuts.

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==UserScript==
// @name         YouTube Playback Speed Control++
// @namespace    https://naeembolchhi.github.io/
// @version      0.6
// @description  Adds an immersive playback speed control interface to YouTube's bottom controls to extend the speed options, while also hiding the default one. Includes keyboard shortcuts.
// @author       NaeemBolchhi
// @license      GPL-3.0-or-later
// @match        https://www.youtube.com/*
// @icon         data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 29 29"><path d="M14.48 24.5s9.08 0 11.34-.6a3.632 3.632 0 0 0 2.56-2.53c.62-2.22.62-6.89.62-6.89s0-4.64-.62-6.84a3.546 3.546 0 0 0-2.56-2.53c-2.25-.61-11.34-.61-11.34-.61s-9.06 0-11.31.61A3.658 3.658 0 0 0 .59 7.64c-.6 2.2-.6 6.84-.6 6.84s0 4.67.6 6.89a3.731 3.731 0 0 0 2.58 2.53c2.24.6 11.31.6 11.31.6Z" fill="%23f03"/><path d="m19 14.5-7.5-4.25v8.5L19 14.5Z" fill="%23fff"/></svg>
// @run-at       document-body
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    // Available speed options
    const speeds = [
        [0.25, 0.5, 0.75, 1],
        [1.25, 1.5, 1.75, 2],
        [2.25, 2.5, 2.75, 3],
        [3.25, 3.5, 3.75, 4]
    ];

    // Define styles
    const speedCSS = `
        .ytp-speed-option, .ytp-speed-increase, .ytp-speed-decrease, .ytp-speed-insert {
            -webkit-user-select: none;
            -ms-user-select: none;
            user-select: none;
            -webkit-tap-highlight-color: transparent;
        }
        .ytp-tooltip.ytp-bottom {
            top: unset !important;
            bottom: 70px;
        }
        .ytp-internal-speed, .ytp-speed-container.hidden {
            display: none !important;
        }
        .ytp-speed-container.invisible {
            opacity: 0;
        }
        .ytp-speed-container, .ytp-speed-row, .ytp-speed-option, .ytp-speed-display, .ytp-speed-increase, .ytp-speed-decrease {
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .ytp-speed-container {
            opacity: 1;
            flex-direction: column;
            position: absolute;
            right: 0;
            bottom: 62px;
            z-index: 9999;
            background: var(--yt-sys-color-baseline--overlay-background-medium,rgba(0,0,0,.6));
            backdrop-filter: var(--yt-frosted-glass-backdrop-filter-override,blur(16px));
            color: rgb(255, 255, 255);
            border-radius: 12px;
            padding: 24px 16px 16px;
            font-size: 14px;
            gap: 8px;
            transition: opacity .1s cubic-bezier(0,0,.2,1);
        }
        .ytp-speed-row {
            gap: 8px;
        }
        .ytp-speed-option {
            cursor: pointer;
            height: 32px;
            width: 53px;
            border-radius: 16px;
            background: rgba(255,255,255,.1);
        }
        .ytp-speed-display {
            font-size: 18px;
            font-weight: 900;
            margin-bottom: 24px;
            width: 80%;
            text-align: center;
        }
        .ytp-speed-insert {
            flex: 1 0 auto;
        }
        .ytp-speed-insert::after,
        .ytp-speed-option::after {
            content: 'x';
        }
        .ytp-speed-increase, .ytp-speed-decrease {
            all: unset;
            cursor: pointer;
            font-size: 24px;
            height: 32px;
            width: 32px;
            border-radius: 100%;
            background: rgba(255,255,255,.1);
        }
        .ytp-speed-option:hover,.ytp-speed-increase:hover, .ytp-speed-decrease:hover {
            background: rgba(255,255,255,.2);
        }
        .ytp-speed-button {
            overflow: visible !important;
            position: relative !important;
            display: flex !important;
            justify-content: center;
        }
        .ytp-speed-button svg {
            transition: transform .1s cubic-bezier(.4,0,1,1);
        }
        .ytp-speed-button.opened svg {
            transform: rotate(30deg);
        }
        .ytp-tooltip-text {
            font-size: 13px;
            width: 92px;
            text-align: center;
        }
        .ytp-speed-tooltip {
            position: absolute;
            bottom: 62px !important;
            display: none;
        }
        .ytp-speed-tooltip.hover {
            display: unset;
        }
        .ytp-right-controls-left:has(>button[aria-expanded="true"]) .ytp-speed-tooltip,
        .ytp-speed-button.opened .ytp-speed-tooltip {
            display: none !important;
        }
        .ytp-chrome-bottom:has(.ytp-speed-button.opened) {
            opacity: 1 !important;
        }
        div:has(>.ytp-bezel-text-wrapper)[class*="visible-"] {
            display: unset !important;
        }
        div:has(>.ytp-bezel-text-wrapper)[class*="visible-"] .ytp-bezel-text-wrapper {
            opacity: 1 !important;
        }
        div:has(>.ytp-bezel-text-wrapper)[class*="visible-"] .ytp-bezel-text-wrapper+* {
            display: none !important;
        }
        #movie_player:not(:has(.ytp-speed-container.invisible)) .ytp-fullscreen-quick-actions {
            display: none !important;
        }
    `;

    // Create speed control
    function createSpeedOptions() {
        // Click hold variable for '+' and '-'
        let holdTimeout, holdInterval;
        const stopHolds = () => {
            clearInterval(holdInterval);
            clearTimeout(holdTimeout);
        };

        const speedContainer = document.createElement('div');
        speedContainer.classList.add('ytp-speed-container','hidden','invisible');

        // Speed display and buttons
        const speedDisplay = document.createElement('div');
        speedDisplay.classList.add('ytp-speed-display');

        const speedDisplaySpan = document.createElement('span');
        speedDisplaySpan.classList.add('ytp-speed-insert');

        const speedDisplayDecrease = document.createElement('button');
        speedDisplayDecrease.classList.add('ytp-speed-decrease');
        speedDisplayDecrease.title = 'Decrease playback speed\nCtrl + , (Comma)';
        speedDisplayDecrease.addEventListener('mousedown', () => {
            changeSpeed('-');
            holdTimeout = setTimeout(() => {
                holdInterval = setInterval(() => {changeSpeed('-')}, 70);
            }, 500);
        });
        speedDisplayDecrease.addEventListener('mouseup', stopHolds);
        speedDisplayDecrease.addEventListener('mouseleave', stopHolds);

        const speedDisplayIncrease = document.createElement('button');
        speedDisplayIncrease.classList.add('ytp-speed-increase');
        speedDisplayIncrease.title = 'Increase playback speed\nCtrl + . (Period)';
        speedDisplayIncrease.addEventListener('mousedown', () => {
            changeSpeed('+');
            holdTimeout = setTimeout(() => {
                holdInterval = setInterval(() => {changeSpeed('+')}, 70);
            }, 500);
        });
        speedDisplayIncrease.addEventListener('mouseup', stopHolds);
        speedDisplayIncrease.addEventListener('mouseleave', stopHolds);

        // Keyboard shortcuts
        document.addEventListener('keydown', (e) => {
            if (e.ctrlKey && e.key === ',') {
                e.preventDefault();
                changeSpeed('-');

            } else if (e.ctrlKey && e.key === '.') {
                changeSpeed('+');

            }
        });

        const speedDisplayDecreaseSpan = document.createElement('span');
        speedDisplayDecreaseSpan.textContent = '–';

        const speedDisplayIncreaseSpan = document.createElement('span');
        speedDisplayIncreaseSpan.textContent = '+';

        speedDisplayDecrease.appendChild(speedDisplayDecreaseSpan);
        speedDisplayIncrease.appendChild(speedDisplayIncreaseSpan);
        speedDisplay.appendChild(speedDisplayDecrease);
        speedDisplay.appendChild(speedDisplaySpan);
        speedDisplay.appendChild(speedDisplayIncrease);

        speedContainer.appendChild(speedDisplay);

        // Add speed options
        for (let x = 0; x < speeds.length; x++) {
            const speedRow = document.createElement('div');
            speedRow.classList.add('ytp-speed-row');

            for (let y = 0; y < speeds[x].length; y++) {
                const speedOption = document.createElement('span');
                speedOption.classList.add('ytp-speed-option');
                speedOption.textContent = speeds[x][y];
                speedOption.addEventListener('click', () => {
                    const video = document.querySelector('video');
                    if (video) {
                        // video.playbackRate = speeds[x][y];
                        setSpeedValue(speedOption.textContent);
                    }
                });
                speedRow.appendChild(speedOption);
            }

            speedContainer.appendChild(speedRow);
        }

        return speedContainer;
    }

    // Create speed button in chromeBottom
    function createSpeedButton() {
        const settingsButton = document.querySelector('.ytp-chrome-bottom .ytp-settings-button');
        const speedContainer = document.querySelector('.ytp-speed-container');

        const speedButton = document.createElement('button');
        speedButton.classList.add('ytp-button', 'ytp-speed-button');

        speedButton.appendChild(settingsButton.children[0].cloneNode(true));
        speedButton.querySelector('path').setAttribute('d', "M12 1c1.44 0 2.87.28 4.21.83a11 11 0 0 1 3.45 2.27l-1.81 1.05c-3.78-3.23-9.46-2.79-12.69.99A8.986 8.986 0 0 0 3 12a9 9 0 0 0 18 0v-.44c-.03-.4-.08-.8-.15-1.2l1.81-1.05c1.49 5.89-2.08 11.87-7.98 13.36-1.36.34-2.78.42-4.17.23-6.02-.82-10.24-6.36-9.42-12.38C1.83 5.06 6.49.99 12 1Zm7.08 6.25-7.96 3.25a1.75 1.75 0 0 0-.92 2.28c.38.88 1.4 1.29 2.28.92.13-.06.25-.13.36-.21l6.8-5.26c.25-.19.29-.55.1-.8a.579.579 0 0 0-.66-.18h-.01Z");

        const speedButtonTooltip = document.createElement('div');
        speedButtonTooltip.classList.add('ytp-tooltip','ytp-bottom','ytp-speed-tooltip');
        const speedButtonTooltip2 = document.createElement('div');
        speedButtonTooltip2.classList.add('ytp-tooltip-text-wrapper');
        const speedButtonTooltip3 = document.createElement('div');
        speedButtonTooltip3.classList.add('ytp-tooltip-bottom-text');
        const speedButtonTooltip4 = document.createElement('span');
        speedButtonTooltip4.classList.add('ytp-tooltip-text');
        speedButtonTooltip4.textContent = 'Playback speed';

        speedButtonTooltip3.appendChild(speedButtonTooltip4);
        speedButtonTooltip2.appendChild(speedButtonTooltip3);
        speedButtonTooltip.appendChild(speedButtonTooltip2);

        speedButton.appendChild(speedButtonTooltip);

        speedButton.addEventListener('click', () => {
            if (speedContainer.classList.contains('hidden')) {
                speedContainer.classList.remove('hidden');

                requestAnimationFrame(() => {
                    requestAnimationFrame(() => {
                        speedContainer.classList.remove('invisible');

                        setTimeout(() => {
                            speedButton.classList.add('opened');
                        }, 100);
                    });
                });
            } else {
                speedContainer.classList.add('invisible');
                speedButton.classList.remove('opened');

                setTimeout(() => {
                    speedContainer.classList.add('hidden');
                }, 100);
            }
        });

        document.body.addEventListener('click', (e) => {
            if (e.target.closest('.ytp-speed-button.ytp-button') || e.target.closest('.ytp-speed-container') || speedContainer.classList.contains('hidden')) {return;}

            speedContainer.classList.add('invisible');
            speedButton.classList.remove('opened');

            setTimeout(() => {
                speedContainer.classList.add('hidden');
            }, 100);
        });

        function hoverState() {
            speedButtonTooltip.classList.add('hover');
        }
        function hoverStateGone() {
            speedButtonTooltip.classList.remove('hover');
        }

        speedButton.addEventListener('mouseenter', () => {
            hoverState();
        });
        speedButton.addEventListener('mouseleave', () => {
            hoverStateGone();
        });
        speedButton.addEventListener('focus', () => {
            hoverState();
        });
        speedButton.addEventListener('blur', () => {
            hoverStateGone();
        });

        settingsButton.parentNode.insertBefore(speedButton, settingsButton);
    }

    // Deal with it when an option is clicked
    function setSpeedValue(speedValue) {
        const displayTextSpan = document.querySelector('.ytp-speed-insert');
        displayTextSpan.textContent = parseFloat(speedValue).toFixed(2);

        const video = document.querySelector('video');
        video.playbackRate = parseFloat(speedValue);

        bezelSpeedValue(speedValue);
    }

    // Display speed value in bezel
    function bezelSpeedValue(speedValue) {
        const bezel = document.querySelector('.ytp-bezel-text-wrapper');
        const videoSpeed = parseFloat(speedValue).toFixed(2);
        const dateNow = parseInt(Date.now()).toString();

        bezel.children[0].textContent = videoSpeed + 'x';
        bezel.parentNode.classList.add('visible-' + dateNow);

        setTimeout(() => {
            bezel.parentNode.classList.remove('visible-' + dateNow)
        }, 1000);
    }

    // Insert speed options into the player
    function insertSpeedOptions() {
        const chromeBottom = document.querySelector('.ytp-chrome-bottom');
        if (!chromeBottom || document.querySelector('.ytp-speed-container')) {return;}

        // Safely add styles
        GM_addStyle(speedCSS);

        // Add speed menu
        chromeBottom.appendChild(createSpeedOptions());

        // Add speed button
        createSpeedButton();

        // Set initial highlight
        setInitialSpeed();
    }

    // Initialize highlight speed options
    function setInitialSpeed() {
        const currentSpeed = document.querySelector('video')?.playbackRate || 1;
        setSpeedValue(currentSpeed);
    }

    // Hide internal speed settings
    function hideInternalSpeed() {
        const chromeBottom = document.querySelector('.ytp-chrome-bottom');
        if (!chromeBottom || document.querySelector('.ytp-internal-speed')) {return;}

        const labels = document.querySelectorAll('#ytp-id-5 .ytp-menuitem-label');

        for (let x = 0; x < labels.length; x++) {
            if (labels[x].textContent.match(/playback speed/i)) {
                labels[x].closest('.ytp-menuitem').classList.add('ytp-internal-speed');
            }
        }
    }

    // Change video speed
    function changeSpeed(key) {
        const changeRate = 0.05;
        const currentSpeed = parseFloat(document.querySelector('video').playbackRate);
        let newSpeed;

        if (key === '-') {
            newSpeed = currentSpeed - changeRate;
        } else if (key === '+') {
            newSpeed = currentSpeed + changeRate;
        }

        if (newSpeed < 0.05) {
            newSpeed = 0.05;
        }

        setSpeedValue(newSpeed);
    }

    // Use MutationObserver to listen for player changes
    const observer = new MutationObserver(() => {
        insertSpeedOptions();
        try {hideInternalSpeed()} catch {}
    });
    observer.observe(document.body, { childList: true, subtree: true });

    // Execute after page loads
    window.addEventListener('load', insertSpeedOptions);
})();