Old Seek UI

Return the old YouTube video player seek UI, replacing the mobile-style ones they added circa 2021.

// ==UserScript==
// @name         Old Seek UI
// @version      0.2
// @description  Return the old YouTube video player seek UI, replacing the mobile-style ones they added circa 2021.
// @author       Taniko Yamamoto
// @author       https://github.com/YukisCoffee/yt-player-classicifier
// @match        https://www.youtube.com/*
// @icon         https://www.youtube.com/favicon.ico
// @grant        none
// @run-at       document-start
// @namespace https://greasyfork.org/users/1132181
// ==/UserScript==

(function() {

    const ICONSET = {
        "back10": "M 18,11 V 7 l -5,5 5,5 v -4 c 3.3,0 6,2.7 6,6 0,3.3 -2.7,6 -6,6 -3.3,0 -6,-2.7 -6,-6 h -2 c 0,4.4 3.6,8 8,8 4.4,0 8,-3.6 8,-8 0,-4.4 -3.6,-8 -8,-8 z M 16.9,22 H 16 V 18.7 L 15,19 v -0.7 l 1.8,-0.6 h .1 V 22 z m 4.3,-1.8 c 0,.3 0,.6 -0.1,.8 l -0.3,.6 c 0,0 -0.3,.3 -0.5,.3 -0.2,0 -0.4,.1 -0.6,.1 -0.2,0 -0.4,0 -0.6,-0.1 -0.2,-0.1 -0.3,-0.2 -0.5,-0.3 -0.2,-0.1 -0.2,-0.3 -0.3,-0.6 -0.1,-0.3 -0.1,-0.5 -0.1,-0.8 v -0.7 c 0,-0.3 0,-0.6 .1,-0.8 l .3,-0.6 c 0,0 .3,-0.3 .5,-0.3 .2,0 .4,-0.1 .6,-0.1 .2,0 .4,0 .6,.1 .2,.1 .3,.2 .5,.3 .2,.1 .2,.3 .3,.6 .1,.3 .1,.5 .1,.8 v .7 z m -0.9,-0.8 v -0.5 c 0,0 -0.1,-0.2 -0.1,-0.3 0,-0.1 -0.1,-0.1 -0.2,-0.2 -0.1,-0.1 -0.2,-0.1 -0.3,-0.1 -0.1,0 -0.2,0 -0.3,.1 l -0.2,.2 c 0,0 -0.1,.2 -0.1,.3 v 2 c 0,0 .1,.2 .1,.3 0,.1 .1,.1 .2,.2 .1,.1 .2,.1 .3,.1 .1,0 .2,0 .3,-0.1 l .2,-0.2 c 0,0 .1,-0.2 .1,-0.3 v -1.5 z",
        "forward10": "m 10,19 c 0,4.4 3.6,8 8,8 4.4,0 8,-3.6 8,-8 h -2 c 0,3.3 -2.7,6 -6,6 -3.3,0 -6,-2.7 -6,-6 0,-3.3 2.7,-6 6,-6 v 4 l 5,-5 -5,-5 v 4 c -4.4,0 -8,3.6 -8,8 z m 6.8,3 H 16 V 18.7 L 15,19 v -0.7 l 1.8,-0.6 h .1 V 22 z m 4.3,-1.8 c 0,.3 0,.6 -0.1,.8 l -0.3,.6 c 0,0 -0.3,.3 -0.5,.3 C 20,21.9 19.8,22 19.6,22 19.4,22 19.2,22 19,21.9 18.8,21.8 18.7,21.7 18.5,21.6 18.3,21.5 18.3,21.3 18.2,21 18.1,20.7 18.1,20.5 18.1,20.2 v -0.7 c 0,-0.3 0,-0.6 .1,-0.8 l .3,-0.6 c 0,0 .3,-0.3 .5,-0.3 .2,0 .4,-0.1 .6,-0.1 .2,0 .4,0 .6,.1 .2,.1 .3,.2 .5,.3 .2,.1 .2,.3 .3,.6 .1,.3 .1,.5 .1,.8 v .7 z m -0.8,-0.8 v -0.5 c 0,0 -0.1,-0.2 -0.1,-0.3 0,-0.1 -0.1,-0.1 -0.2,-0.2 -0.1,-0.1 -0.2,-0.1 -0.3,-0.1 -0.1,0 -0.2,0 -0.3,.1 l -0.2,.2 c 0,0 -0.1,.2 -0.1,.3 v 2 c 0,0 .1,.2 .1,.3 0,.1 .1,.1 .2,.2 .1,.1 .2,.1 .3,.1 .1,0 .2,0 .3,-0.1 l .2,-0.2 c 0,0 .1,-0.2 .1,-0.3 v -1.5 z",
        "back5": "M 18,11 V 7 l -5,5 5,5 v -4 c 3.3,0 6,2.7 6,6 0,3.3 -2.7,6 -6,6 -3.3,0 -6,-2.7 -6,-6 h -2 c 0,4.4 3.6,8 8,8 4.4,0 8,-3.6 8,-8 0,-4.4 -3.6,-8 -8,-8 z m -1.3,8.9 .2,-2.2 h 2.4 v .7 h -1.7 l -0.1,.9 c 0,0 .1,0 .1,-0.1 0,-0.1 .1,0 .1,-0.1 0,-0.1 .1,0 .2,0 h .2 c .2,0 .4,0 .5,.1 .1,.1 .3,.2 .4,.3 .1,.1 .2,.3 .3,.5 .1,.2 .1,.4 .1,.6 0,.2 0,.4 -0.1,.5 -0.1,.1 -0.1,.3 -0.3,.5 -0.2,.2 -0.3,.2 -0.4,.3 C 18.5,22 18.2,22 18,22 17.8,22 17.6,22 17.5,21.9 17.4,21.8 17.2,21.8 17,21.7 16.8,21.6 16.8,21.5 16.7,21.3 16.6,21.1 16.6,21 16.6,20.8 h .8 c 0,.2 .1,.3 .2,.4 .1,.1 .2,.1 .4,.1 .1,0 .2,0 .3,-0.1 L 18.5,21 c 0,0 .1,-0.2 .1,-0.3 v -0.6 l -0.1,-0.2 -0.2,-0.2 c 0,0 -0.2,-0.1 -0.3,-0.1 h -0.2 c 0,0 -0.1,0 -0.2,.1 -0.1,.1 -0.1,0 -0.1,.1 0,.1 -0.1,.1 -0.1,.1 h -0.7 z",
        "forward5": "m 10,19 c 0,4.4 3.6,8 8,8 4.4,0 8,-3.6 8,-8 h -2 c 0,3.3 -2.7,6 -6,6 -3.3,0 -6,-2.7 -6,-6 0,-3.3 2.7,-6 6,-6 v 4 l 5,-5 -5,-5 v 4 c -4.4,0 -8,3.6 -8,8 z m 6.7,.9 .2,-2.2 h 2.4 v .7 h -1.7 l -0.1,.9 c 0,0 .1,0 .1,-0.1 0,-0.1 .1,0 .1,-0.1 0,-0.1 .1,0 .2,0 h .2 c .2,0 .4,0 .5,.1 .1,.1 .3,.2 .4,.3 .1,.1 .2,.3 .3,.5 .1,.2 .1,.4 .1,.6 0,.2 0,.4 -0.1,.5 -0.1,.1 -0.1,.3 -0.3,.5 -0.2,.2 -0.3,.2 -0.5,.3 C 18.3,22 18.1,22 17.9,22 17.7,22 17.5,22 17.4,21.9 17.3,21.8 17.1,21.8 16.9,21.7 16.7,21.6 16.7,21.5 16.6,21.3 16.5,21.1 16.5,21 16.5,20.8 h .8 c 0,.2 .1,.3 .2,.4 .1,.1 .2,.1 .4,.1 .1,0 .2,0 .3,-0.1 L 18.4,21 c 0,0 .1,-0.2 .1,-0.3 v -0.6 l -0.1,-0.2 -0.2,-0.2 c 0,0 -0.2,-0.1 -0.3,-0.1 h -0.2 c 0,0 -0.1,0 -0.2,.1 -0.1,.1 -0.1,0 -0.1,.1 0,.1 -0.1,.1 -0.1,.1 h -0.6 z",
        "backchapter": "m 16.436975,17.634454 c -0.573529,0 -1.191177,0.117647 -1.617647,0.441177 v 4.308938 c 0,0.191177 0.214706,0.132123 0.220588,0.132123 0.397059,-0.191176 0.970588,-0.323414 1.397059,-0.323414 0.57353,0 1.191177,0.117646 1.617647,0.441176 0.397059,-0.25 1.117648,-0.441176 1.617647,-0.441176 0.485295,0 0.985294,0.08846 1.397059,0.309053 0.120588,0.06177 0.220589,-0.05623 0.220589,-0.132698 v -4.294002 c -0.438235,-0.329412 -1.067647,-0.441177 -1.617648,-0.441177 -0.573529,0 -1.191176,0.117647 -1.617647,0.441177 -0.42647,-0.32353 -1.044117,-0.441177 -1.617647,-0.441177 z m 3.235294,0.588235 c 0.352942,0 0.705883,0.04411 1.029412,0.147059 v 3.382353 c -0.323529,-0.102941 -0.67647,-0.147059 -1.029412,-0.147059 -0.499999,0 -1.220588,0.191177 -1.617647,0.441177 v -3.382353 c 0.397059,-0.25 1.117648,-0.441177 1.617647,-0.441177 z m -0.674976,1.202322 v 1.303997 l 1.024241,-0.651999 z M 18,7 l -5,5 5,5 v -4 c 3.3,0 6,2.7 6,6 0,3.3 -2.7,6 -6,6 -3.3,0 -6,-2.7 -6,-6 h -2 c 0,4.4 3.6,8 8,8 4.4,0 8,-3.6 8,-8 0,-4.4 -3.6,-8 -8,-8 z",
        "forwardchapter": "m 16.436975,17.634454 c -0.573529,0 -1.191177,0.117647 -1.617647,0.441177 v 4.308938 c 0,0.191177 0.214706,0.132123 0.220588,0.132123 0.397059,-0.191176 0.970588,-0.323414 1.397059,-0.323414 0.57353,0 1.191177,0.117646 1.617647,0.441176 0.397059,-0.25 1.117648,-0.441176 1.617647,-0.441176 0.485295,0 0.985294,0.08846 1.397059,0.309053 0.120588,0.06177 0.220589,-0.05623 0.220589,-0.132698 v -4.294002 c -0.438235,-0.329412 -1.067647,-0.441177 -1.617648,-0.441177 -0.573529,0 -1.191176,0.117647 -1.617647,0.441177 -0.42647,-0.32353 -1.044117,-0.441177 -1.617647,-0.441177 z m 3.235294,0.588235 c 0.352942,0 0.705883,0.04411 1.029412,0.147059 v 3.382353 c -0.323529,-0.102941 -0.67647,-0.147059 -1.029412,-0.147059 -0.499999,0 -1.220588,0.191177 -1.617647,0.441177 v -3.382353 c 0.397059,-0.25 1.117648,-0.441177 1.617647,-0.441177 z m -0.674976,1.202322 v 1.303997 l 1.024241,-0.651999 z M 18,7 v 4 c -4.4,0 -8,3.6 -8,8 0,4.4 3.6,8 8,8 4.4,0 8,-3.6 8,-8 h -2 c 0,3.3 -2.7,6 -6,6 -3.3,0 -6,-2.7 -6,-6 0,-3.3 2.7,-6 6,-6 v 4 l 5,-5 z"
    };
    const DEBUG = false;

    // Player API reference
    var api;
    var bezel;

    var animationStartTimer = 0;
    var animationShouldEndTimer = 0;

    function log(a)
    {
        if (DEBUG) console.log(a);
    }

    function getAnimationDuration(elm)
    {
        var prop = window.getComputedStyle(elm).animationDuration;

        switch (true)
        {
            case "ms" == prop.substr(-2):
                return +prop.replace("ms", "");
            case "s" == prop.substr(-1):
                return +(prop.replace("s", "")) * 1000;
        }
    }

    async function attemptEndAnimateBezel()
    {
        while (animationShouldEndTimer > Date.now())
        {
            await new Promise(r => requestAnimationFrame(r));
        }

        animationStartTimer = 0;
        animationShouldEndTimer = 0;
        bezel.style.display = "none";
    }

    function animateBezel()
    {
        var animationDuration = getAnimationDuration(bezel.querySelector(".ytp-bezel"));

        bezel.style.display = "";

        if (0 == animationShouldEndTimer)
        {
            animationStartTimer = Date.now();
            animationShouldEndTimer = animationStartTimer;
        }

        animationShouldEndTimer += animationDuration;

        attemptEndAnimateBezel();
    }

    function waitToAnimate()
    {
        return new Promise(resolve => {
            setTimeout(() => {
                resolve();
            }, 10);
        });
    }

    function createBezel(direction, duration, text = "", chapter = false)
    {
        return new Promise(resolve => {
            log("Creating bezel");
            var bezelElm = api.querySelector(".ytp-bezel");
            bezel = bezelElm.parentNode;

            bezelElm.removeAttribute("aria-label");

            // Get the icon from the iconset
            var icon;
            if (chapter)
            {
                icon = ICONSET[direction + "chapter"];
            }
            else if (ICONSET[direction + duration])
            {
                icon = ICONSET[direction + duration];
            }
            else
            {
                icon = "";
            }

            var iconElm;
            if (iconElm = bezel.querySelector(".ytp-bezel-icon path"))
            {
                iconElm.setAttribute("d", icon);
            }
            else
            {
                bezel.querySelector(".ytp-bezel-icon").insertAdjacentHTML("beforeend",
                    `<svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%">
                        <path class="ytp-svg-fill" d="${icon}"></path>
                    </svg>`
                );
            }

            if ("" === text)
            {
                bezel.setAttribute("class", "ytp-bezel-text-hide");
            }
            else
            {
                bezel.setAttribute("class", "");
                bezel.querySelector(".ytp-bezel-text").innerText = text;
            }

            bezel.style.display = "none";

            resolve();
        });
    }

    async function waitForElement(query, timeout = 500)
    {
        log("Waiting for element " + query + " with timeout in " + timeout + " ms.");
        var hasTimedOut = false;

        setTimeout(function() {
            log("Wait for element " + query + " has timed out.");
            hasTimedOut = true;
        }, timeout);

        while (null == document.querySelector(query) && !hasTimedOut)
        {
            await new Promise(r => requestAnimationFrame(r));
        }

        var a;
        if (a = document.querySelector(query))
        {
            return a;
        }
        else
        {
            return null;
        }
    }

    function handleSeekGui()
    {
        log("Handing seek GUI");
        var direction = this.dataset.side;
        var duration = this.querySelector(".ytp-doubletap-tooltip-label")
            .innerText.replace(/[\s|[A-Za-z]]*/g, "")
        ;
        var text = "";
        var isChapter = false;

        if (this.classList.contains("ytp-chapter-seek"))
        {
            var textContainer = this.querySelector(".ytp-chapter-seek-text-legacy")
            text = textContainer.innerText;
            duration = 0;
            isChapter = true;
        }

        createBezel(direction, duration, text, isChapter).then(waitToAnimate).then(animateBezel);
    }

    async function attemptHookPlayer()
    {
        log("Attempting to hook player");
        var playerApi = await waitForElement(".html5-video-player", 5000);

        if (playerApi)
        {
            log("Player API detected");
            api = playerApi;

            var doubleTapElm = api.querySelector(".ytp-doubletap-ui-legacy") ?? api.querySelector(".ytp-doubletap-ui") ?? null;

            if (doubleTapElm && !api.__oldSeekUi)
            {
                log("Doubletap detected: installing binding");
                (new MutationObserver(handleSeekGui.bind(doubleTapElm)))
                    .observe(doubleTapElm, {"subtree": true, "childList": true, "characterData": "true"});
                api.__oldSeekUi = true;
            }
        }
    }

    function insertContinuationEvent()
    {
        log("Inserting continuation events");
        if (window.ytspf && window.ytspf.enabled)
        {
            log("Inserted spf continuation events");
            document.addEventListener("spfdone", attemptHookPlayer);
        }
        else if (document.querySelector("ytd-app"))
        {
            log("Inserted kevlar continuation events");
            document.addEventListener("yt-page-data-updated", attemptHookPlayer);
        }
    }

    function installInitialStyles()
    {
        log("Installed initial styles");
        document.head.insertAdjacentHTML("beforeend",
            `<style>
                .ytp-doubletap-ui, .ytp-doubletap-ui-legacy
                {
                    display: none !important;
                }
            </style>`
        );
    }

    function handleDOMContentLoaded()
    {
        log("domcontentloaded event fired");
        installInitialStyles();
        document.removeEventListener("DOMContentLoaded", handleDOMContentLoaded);
    }

    async function main()
    {
        log("Old seek ui script loaded");

        document.addEventListener("DOMContentLoaded", handleDOMContentLoaded);

        // The player needs more time to init
        // So wait until a little while after page load to attempt
        // hooking the player
        window.addEventListener("load", function handleLoad() {
            log("load event fired");
            attemptHookPlayer();
            insertContinuationEvent();
            window.removeEventListener("load", handleLoad);
        });
    }

    main();

})();