YouTube Sizer

Resizes the YouTube player to smaller sizes

/*
    Resizes the YouTube player to smaller sizes.
    Copyright (C) 2023 John Burt

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program. If not, see <http://www.gnu.org/licenses/>
*/
// ==UserScript==
// @name          YouTube Sizer
// @author        John Burt
// @namespace     namespace_runio
// @version       1.96
// @description   Resizes the YouTube player to smaller sizes
// @match         https://www.youtube.com/*
// @match         https://www.youtu.be/*
// @exclude       https://www.youtube.com/tv*
// @exclude       https://www.youtube.com/embed/*
// @exclude       https://www.youtube.com/live_chat*
// @exclude       https://www.youtube.com/shorts/*
// @run-at        document-end
// @grant         GM_setValue
// @grant         GM_getValue
// @supportURL    https://greasyfork.org/scripts/421396-youtube-sizer
// @icon          https://i.imgur.com/9haPE5X.png
// @license       GPL-3.0+
// @noframes
// ==/UserScript==
(function() {
    "use strict";
    //==================================================================
    //Local Storage Functions
    if (window.frameElement) {
        throw new Error("Stopped JavaScript.")
    }

    function setPref(preference, new_value) {
        GM_setValue(preference, new_value);
    }

    function getPref(preference) {
        return GM_getValue(preference);
    }

    function initPref(preference, new_value) {
        let value = getPref(preference);
        if (value === null || isNaN(value)) {
            setPref(preference, new_value);
            value = new_value;
        }
        return value;
    }
    //==================================================================
    initPref("yt-width", 854);
    initPref("yt-resize", false);
    //==================================================================
    // Global Variables
    var maxWidth = getPref("yt-width"); // Max Width of Video
    var shortcutKey = "r"; // Shortcut Key
    var ytresizeCss = `.ytp-big-mode .ytp-chrome-controls .ytp-resize-button { display:none !important; }`;
    //==================================================================
    window.addEventListener("load", () => {
        const observer = new MutationObserver(checkURL);
        const e = document.querySelector("title");
        if (e) {
            observer.observe(e, {
                attributes: true,
                characterData: true,
                childList: true
            });
        }
    }, {
        once: true
    });
    //==================================================================
    function checkURL() {
        waitElement("#player-container-outer").then((elm) => {
            if (window.location.pathname.includes("watch")) {
                if (!document.getElementById("yt-css")) {
                    startMethods();
                }
                createResize();
            }
        });
    }
    //==================================================================
    function waitElement(selector) {
        const element = document.querySelector(selector);
        const targetNode = document.body;
        const config = {
            childList: true,
            subtree: true
        };
        return new Promise((resolve) => {
            const observer = new MutationObserver((mutationsList) => {
                for (let mutation of mutationsList) {
                    if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
                        observer.disconnect();
                        return resolve(element);
                    }
                }
            });
            observer.observe(targetNode, config);
        });
    }
    //==================================================================
    function startMethods() {
        new Promise((resolve) => {
            sizeObserver(); // Size Observer
            resolve();
        }).then(() => {
            if (getPref("yt-resize")) {
                addCss(`#primary.ytd-watch-flexy:not([theater]):not([fullscreen]) { max-width: ${maxWidth}px !important; }`, "small-player");
            }
            addCss(ytresizeCss, "yt-css");
            controlResize(); // Create Resize Button
            viewObserver(); // Video Container Observer
        });
    }
    //==================================================================
    function isCentered(element1, element2) {
        const box1 = element1.getBoundingClientRect();
        const box2 = element2.getBoundingClientRect();
        const center1 = {
            x: box1.left + box1.width / 2,
            y: box1.top + box1.height / 2
        };
        const center2 = {
            x: box2.left + box2.width / 2,
            y: box2.top + box2.height / 2
        };
        const horizontalDistance = Math.abs(center1.x - center2.x);
        const verticalDistance = Math.abs(center1.y - center2.y);

        return horizontalDistance <= 1 && verticalDistance <= 1;
    }

    function addCss(cssString, id) {
        const css = document.createElement("style");
        css.type = "text/css";
        css.id = id;
        css.innerHTML = cssString;
        document.head.appendChild(css);
    }
    //==================================================================
    /*Resize Video Container*/
    function createResize() {
        const element = document.querySelector("ytd-app");
        element.dispatchEvent(
            new CustomEvent("yt-action", {
                bubbles: !0,
                cancelable: !0,
                composed: !0,
                detail: {
                    actionName: "yt-window-resized",
                    disableBroadcast: false,
                    optionalAction: true,
                    returnValue: []
                }
            })
        );
        return;
    }
    /*Viewport Observer*/
    function viewObserver() {
        let movie_player = document.querySelector(".html5-video-player");
        let video = document.querySelector("video");
        let isCenteredMoviePlayer = isCentered(video, movie_player);
        let resizeObserver = new ResizeObserver((entries) => {
            window.requestAnimationFrame(() => {
                if (!Array.isArray(entries) || !entries.length) { // Check Animation Frame
                    return;
                }
                for (let entry of entries) {
                    if (!isCenteredMoviePlayer) {
                        if (entry.contentRect.width !== maxWidth) {
                            createResize();
                            console.log("Player Size: " + entry.contentRect.width + " x " + entry.contentRect.height);
                        }
                    }
                }
            });
        });
        // observe the given element for changes
        resizeObserver.observe(video);
    }
    //==================================================================
    /*Saves Size Setting*/
    function sizeObserver() {
        const targetNode = document.head;
        const config = {
            attributes: true,
            childList: true,
            subtree: true,
            characterData: true
        };
        const callback = function(mutationsList, observer) {
            for (let mutation of mutationsList) {
                if (mutation.removedNodes.length >= 1 && mutation.removedNodes[0].id == "small-player") {
                    setPref("yt-resize", false); // Set Resize To False
                    controlResize(); // Change Button Icon
                    createResize(); // Resize Video Container
                } else if (mutation.addedNodes.length >= 1 && mutation.addedNodes[0].id == "small-player") {
                    setPref("yt-resize", true); // Set Resize To True
                    controlResize(); // Change Button Icon
                    createResize(); // Resize Video Container
                }
                if (mutation.target.id === "small-player" || mutation.target.parentNode.id === "small-player") {
                    setPref("yt-width", maxWidth);
                    createResize(); // Resize Video Container
                }
            }
        };
        const observer = new MutationObserver(callback);
        observer.observe(targetNode, config);
    }
    //==================================================================
    function showResizeButtonTooltip(btn, show = true) {
        let tooltipTopOffset = 62; // Height Above The Button For The Tooltip
        const buttonRect = btn.getBoundingClientRect(); // Get Button Position
        const tooltipHorizontalCenter = buttonRect.left + buttonRect.width / 2; // Tooltip Horizontal Center
        const tooltipTop = buttonRect.top + buttonRect.height / 2 - tooltipTopOffset; // Tooltip Top
        const tooltip = document.getElementById("ytd-resize-tt") || createTooltip();
        const tooltipText = tooltip.querySelector("#ytd-resize-tt-text");
        if (show) { // Show
            tooltip.style.top = `${tooltipTop}px`;
            tooltipText.textContent = btn.getAttribute("aria-label");
            tooltip.style.removeProperty("display");
            const tooltipWidth = tooltip.getBoundingClientRect().width;
            tooltip.style.left = `${tooltipHorizontalCenter - tooltipWidth / 2}px`;
            btn.removeAttribute("title");
        } else { // Hide
            tooltip.style.setProperty("display", "none");
            tooltipText.textContent = "";
            btn.setAttribute("title", btn.getAttribute("aria-label"));
        }

        function createTooltip() {
            const htmlPlayer = document.querySelector(".html5-video-player");
            const tooltip = document.createElement("div");
            const tooltipTextWrapper = document.createElement("div");
            const tooltipText = document.createElement("span");
            tooltip.setAttribute("class", "ytp-tooltip ytp-bottom");
            tooltip.setAttribute("id", "ytd-resize-tt");
            tooltip.style.setProperty("position", "fixed");
            tooltipTextWrapper.setAttribute("class", "ytp-tooltip-text-wrapper");
            tooltipText.setAttribute("class", "ytp-tooltip-text");
            tooltipText.setAttribute("id", "ytd-resize-tt-text");
            tooltip.appendChild(tooltipTextWrapper);
            tooltipTextWrapper.appendChild(tooltipText);
            htmlPlayer.appendChild(tooltip);
            return tooltip;
        }
    }
    //==================================================================
    /*Resize Button Script*/
    function setButton(btn, path) {
        var pathData = {};
        var ariaLabel = "";
        var title = "";
        if (!getPref("yt-resize")) {
            pathData.d = `M 19 23 L 11 15 L 11 23 Z M 29 25
                          L 29 10.98 C 29 9.88 28.1 9 27 9
                          L 9 9 C 7.9 9 7 9.88 7 10.98
                          L 7 25 C 7 26.1 7.9 27 9 27
                          L 27 27 C 28.1 27 29 26.1 29 25
                          L 29 25 Z M 27 25.02 L 9 25.02 L 9 10.97
                          L 27 10.97 L 27 25.02 L 27 25.02 Z`;
            ariaLabel = `Resize mode (${shortcutKey})`;
            title = `Resize mode (${shortcutKey})`;
        } else {
            pathData.d = `M 25 21 L 25 13 L 17 13 Z M 29 25
                          L 29 10.98 C 29 9.88 28.1 9 27 9
                          L 9 9 C 7.9 9 7 9.88 7 10.98
                          L 7 25 C 7 26.1 7.9 27 9 27
                          L 27 27 C 28.1 27 29 26.1 29 25
                          L 29 25 Z M 27 25.02
                          L 9 25.02 L 9 10.97 L 27 10.97
                          L 27 25.02 L 27 25.02 Z`;
            ariaLabel = `Default view (${shortcutKey})`;
            title = `Default view (${shortcutKey})`;
        }
        path.setAttribute("d", pathData.d);
        btn.setAttribute("aria-label", ariaLabel);
        btn.setAttribute("title", title);
    }

    function createButton() {
        let abtn = document.querySelector(".ytp-right-controls");
        let btn = document.createElement("button");
        let path = document.createElement("path");
        let clickEvent = new Event("click", {
            bubbles: false
        });

        /*Start Create SVG*/
        let svg = document.createElement("svg");
        svg.setAttribute("height", "100%");
        svg.setAttribute("version", "1.1");
        svg.setAttribute("viewBox", "0 0 36 36");
        svg.setAttribute("width", "100%");
        let use = document.createElement("use");
        use.setAttribute("class", "ytp-svg-shadow");

        setButton(btn, path); // Decide Which Button

        path.setAttribute("fill", "#fff");
        path.setAttribute("fill-rule", "evenodd");
        svg.appendChild(use);
        svg.appendChild(path);

        const btnContent = svg.outerHTML;
        /*Finished Create SVG*/

        btn.innerHTML = btnContent;
        btn.classList.add("ytp-resize-button", "ytp-button");
        btn.setAttribute("id", "ytp-resize-button");
        btn.setAttribute("data-tooltip-target-id", "ytp-resize-button");
        abtn.insertBefore(btn, abtn.lastChild.previousSibling);

        /*Tooltip Event Handlers*/
        const showTooltip = (event) => {
            const isMouseOver = ["mouseover", "focus"].includes(event.type);
            showResizeButtonTooltip(btn, isMouseOver);
        };
        btn.addEventListener("click", function(e) {
            e.stopPropagation();
            e.preventDefault();
            buttonScript();
        }, clickEvent);
        btn.addEventListener("mouseover", showTooltip);
        btn.addEventListener("mouseout", showTooltip);
        btn.addEventListener("focus", showTooltip);
        btn.addEventListener("blur", showTooltip);
    }

    function buttonScript() {
        let splayer = document.getElementById("small-player");
        if (document.head.contains(splayer)) {
            document.head.removeChild(splayer);
        } else {
            addCss(`#primary.ytd-watch-flexy:not([theater]):not([fullscreen]) { max-width: ${maxWidth}px !important; }`, "small-player");
        }
        return;
    }

    function shortScript() {
        let splayer = document.getElementById("small-player");
        if (document.head.contains(splayer)) {
            splayer.innerHTML = `#primary.ytd-watch-flexy:not([theater]):not([fullscreen]) { max-width: ${maxWidth}px !important; }`;
        } else {
            addCss(smallerCss, "small-player");
        }
        return;
    }
    /*Create Resize Button*/
    function controlResize() {
        let buttonExists = document.getElementById("ytp-resize-button");
        if (!buttonExists) {
            createButton();
            document.addEventListener("keydown", function(e) {
                let splayer = document.getElementById("small-player");
                // Common checks for all keys
                if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
                if (/^(?:input|textarea|select|button)$/i.test(e.target.tagName)) return;
                if (/(?:contenteditable-root)/i.test(e.target.id)) return;
                // Check for shortcutKey
                if (e.key == shortcutKey.toLowerCase() || e.key == shortcutKey.toUpperCase()) {
                    e.stopPropagation();
                    e.preventDefault();
                    buttonScript();
                    return;
                }
                // Check for "x" and "z" if "small-player" is in the document head
                if (document.head.contains(splayer)) {
                    if (e.key == "z") {
                        e.stopPropagation();
                        e.preventDefault();
                        maxWidth -= 20;
                        if (maxWidth <= 854) maxWidth = 854; // min-width
                        shortScript();
                    } else if (e.key == "x") {
                        e.stopPropagation();
                        e.preventDefault();
                        maxWidth += 20;
                        if (maxWidth >= window.innerWidth) maxWidth = window.innerWidth; // max-width
                        shortScript();
                    }
                }
            });
            document.addEventListener("wheel", function(e) {
                let splayer = document.getElementById("small-player");
                let player = document.getElementById("primary-inner");
                if (document.head.contains(splayer)) {
                    if (e.altKey || e.ctrlKey || e.metaKey) return;
                    if (/^(?:input|textarea|select|button)$/i.test(e.target.tagName)) return;
                    if (/(?:contenteditable-root)/i.test(e.target.id)) return;
                    if (e.shiftKey) {
                        e.stopPropagation();
                        if (e.deltaY < 0) { // Scroll Up
                            maxWidth += 20;
                            if (maxWidth >= window.innerWidth) maxWidth = window.innerWidth; // max-width
                        } else if (e.deltaY > 0) { // Scroll Down
                            maxWidth -= 20;
                            if (maxWidth <= 854) maxWidth = 854; // min-width
                        }
                        shortScript();
                    }
                }
            });
        } else {
            setButton(buttonExists, buttonExists.querySelector("path"));
        }
    }
    //==================================================================
})();