YouTube Sizer

Resizes the YouTube player to different sizes

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name YouTube Sizer
// @author John Burt
// @namespace namespace_runio
// @version 2.04
// @description Resizes the YouTube player to different 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";

    if (window.frameElement) {
        throw new Error("Stopped JavaScript.");
    }
    //==================================================================
    // Storage helpers
    //==================================================================
    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 || value === undefined) {
            setPref(preference, new_value);
            value = new_value;
        } else if (typeof value === "number" && isNaN(value)) {
            console.warn(`[YT Sizer] Stored preference "${preference}" was NaN — resetting to default.`);
            setPref(preference, new_value);
            value = new_value;
        }
        return value;
    }
    //==================================================================
    // Preferences
    //==================================================================
    initPref("yt-width", 1280);
    initPref("yt-resize", false);

    function getMaxWidth() {
        const stored = getPref("yt-width") ?? 854;
        return Math.max(854, Math.min(stored, window.innerWidth));
    }

    function setMaxWidth(value) {
        const clamped = Math.max(854, Math.min(value, window.innerWidth));
        setPref("yt-width", clamped);
    }
    //==================================================================
    // Global constants / state
    //==================================================================
    var shortcutKey = "R";
    var ytresizeCss = `ytd-watch-flexy[fullscreen] #ytp-resize-button { display:none !important; }`;
    let currentResizeObserver = null;
    let keyListenersAdded = false;
    let sizeObserverInstance = null;
    let resizeDebounceTimer = null;
    let checkURLDebounceTimer = null;
    let lastCheckedPath = "";
    //==================================================================
    // Boot
    //==================================================================
    window.addEventListener("load", () => {
        const observer = new MutationObserver(checkURL);
        const titleEl = document.querySelector("title");
        if (titleEl) {
            observer.observe(titleEl, {
                attributes: true,
                characterData: true,
                childList: true
            });
            checkURL();
        }
    }, {
        once: true
    });
    //==================================================================
    // checkURL
    //==================================================================
    function checkURL() {
        clearTimeout(checkURLDebounceTimer);
        checkURLDebounceTimer = setTimeout(() => {
            const path = window.location.pathname;
            if (!path.includes("watch")) return;
            if (path === lastCheckedPath) return;
            lastCheckedPath = path;
            waitElement("#player-container-outer").then((elm) => {
                if (!window.location.pathname.includes("watch")) return;
                if (!document.getElementById("yt-css")) {
                    startMethods();
                } else {
                    controlResize();
                    createResize();
                    viewObserver();
                }
            }).catch(() => {});
        }, 150);
    }
    //==================================================================
    // waitElement
    //==================================================================
    function waitElement(selector, timeoutMs = 5000) {
        return new Promise((resolve, reject) => {
            let element = document.querySelector(selector);
            if (element) return resolve(element);
            const observer = new MutationObserver(() => {
                element = document.querySelector(selector);
                if (element) {
                    clearTimeout(timer);
                    observer.disconnect();
                    resolve(element);
                }
            });
            const timer = setTimeout(() => {
                observer.disconnect();
                reject(new Error(`waitElement: "${selector}" not found within ${timeoutMs} ms`));
            }, timeoutMs);
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        });
    }
    //==================================================================
    // startMethods
    //==================================================================
    function startMethods() {
        sizeObserver();
        if (getPref("yt-resize") === true) {
            addCss(`#primary.ytd-watch-flexy:not([theater]):not([fullscreen]) { max-width: ${getMaxWidth()}px !important; }`, "small-player");
            addCss(`ytd-watch-flexy[flexy]:not([full-bleed-player][full-bleed-no-max-width-columns]) #columns.ytd-watch-flexy {max-width: 100% !important;}`, "max-player");
        }
        addCss(ytresizeCss, "yt-css");
        controlResize();
        viewObserver();
    }
    //==================================================================
    // Helpers
    //==================================================================
    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
        };
        return Math.abs(center1.x - center2.x) <= 1 &&
            Math.abs(center1.y - center2.y) <= 1;
    }

    function addCss(cssString, id) {
        const css = document.createElement("style");
        css.type = "text/css";
        css.id = id;
        css.textContent = cssString;
        document.head.appendChild(css);
    }
    //==================================================================
    // createResize
    //==================================================================
    function createResize() {
        const element = document.querySelector("ytd-app");
        if (!element) return;
        element.dispatchEvent(new CustomEvent("yt-action", {
            bubbles: true,
            cancelable: true,
            composed: true,
            detail: {
                actionName: "yt-window-resized",
                disableBroadcast: false,
                optionalAction: true,
                returnValue: []
            }
        }));
    }
    //==================================================================
    // viewObserver
    //==================================================================
    function viewObserver() {
        let movie_player = document.querySelector(".html5-video-player");
        let video = document.querySelector("video");
        if (!movie_player || !video) return;
        if (currentResizeObserver) currentResizeObserver.disconnect();
        const resizeObserver = new ResizeObserver((entries) => {
            window.requestAnimationFrame(() => {
                if (!Array.isArray(entries) || !entries.length) return;
                const currentlyCentered = isCentered(video, movie_player);
                if (!currentlyCentered) {
                    clearTimeout(resizeDebounceTimer);
                    resizeDebounceTimer = setTimeout(createResize, 100);
                }
            });
        });
        currentResizeObserver = resizeObserver;
        resizeObserver.observe(video);
    }
    //==================================================================
    // sizeObserver
    //==================================================================
    function sizeObserver() {
        if (sizeObserverInstance) return;
        const config = {
            attributes: true,
            childList: true,
            subtree: true,
            characterData: true
        };
        const callback = function(mutationsList) {
            for (let mutation of mutationsList) {
                const removedHasSmall = Array.from(mutation.removedNodes).some(
                    node => node.nodeType === Node.ELEMENT_NODE && node.id === "small-player"
                );
                const addedHasSmall = Array.from(mutation.addedNodes).some(
                    node => node.nodeType === Node.ELEMENT_NODE && node.id === "small-player"
                );
                if (removedHasSmall) {
                    setPref("yt-resize", false);
                    controlResize();
                    createResize();
                } else if (addedHasSmall) {
                    setPref("yt-resize", true);
                    controlResize();
                    createResize();
                }
                if (mutation.target && mutation.target.id === "small-player") {
                    setPref("yt-width", getMaxWidth());
                    createResize();
                } else if (mutation.target && mutation.target.parentNode &&
                    mutation.target.parentNode.id === "small-player") {
                    setPref("yt-width", getMaxWidth());
                    createResize();
                }
            }
        };
        sizeObserverInstance = new MutationObserver(callback);
        sizeObserverInstance.observe(document.head, config);
    }
    //==================================================================
    // Tooltip — R is now inside a real bordered box
    //==================================================================
    let resizeTooltipHideTimeout = null;

    function showResizeButtonTooltip(btn, show = true) {
        if (resizeTooltipHideTimeout) {
            clearTimeout(resizeTooltipHideTimeout);
            resizeTooltipHideTimeout = null;
        }

        const buttonRect = btn.getBoundingClientRect();
        const tooltipHorizontalCenter = buttonRect.left + buttonRect.width / 2;

        const tooltip = document.getElementById("ytd-resize-tt") || createTooltip();
        const tooltipText = tooltip.querySelector("#ytd-resize-tt-text");
        const tooltipKey = tooltip.querySelector("#ytd-resize-tt-key");

        if (show) {
            const label = btn.getAttribute("aria-label") || btn.getAttribute("title") || "Resize";
            tooltipText.textContent = label.replace(/\s*\[[^\]]+\]\s*$/, "");
            tooltipKey.textContent = shortcutKey;

            tooltip.style.removeProperty("display");
            tooltip.style.visibility = "hidden";
            tooltip.style.transition = "none";
            void tooltip.offsetHeight;

            const tooltipRect = tooltip.getBoundingClientRect();

            // --- FIX: anchor to stable YouTube UI baseline ---
            const controlsBar =
                  document.querySelector(".ytp-chrome-bottom") ||
                  document.querySelector(".ytp-progress-bar-container");
            let baseTop;
            if (controlsBar) {
                const controlsRect = controlsBar.getBoundingClientRect();
                baseTop = controlsRect.top;
            } else {
                baseTop = buttonRect.top;
            }

            const gap = 11.5;
            const tooltipTop = baseTop - tooltipRect.height - gap;

            tooltip.style.top = `${tooltipTop}px`;
            tooltip.style.left = `${tooltipHorizontalCenter - tooltipRect.width / 2}px`;
            tooltip.style.visibility = "visible";
            btn.removeAttribute("title");
        } else {
            resizeTooltipHideTimeout = setTimeout(() => {
                tooltip.style.display = "none";
                tooltip.style.visibility = "";
                tooltipText.textContent = "";
                tooltipKey.textContent = "";
                const currentLabel = btn.getAttribute("aria-label");
                if (currentLabel) {
                    btn.setAttribute("title", currentLabel);
                }
                resizeTooltipHideTimeout = null;
            }, 120);
        }

        function createTooltip() {
            const htmlPlayer = document.querySelector(".html5-video-player");
            if (!htmlPlayer) {
                const fallback = document.createElement("div");
                fallback.style.display = "none";
                return fallback;
            }

            const tooltip = document.createElement("div");
            tooltip.id = "ytd-resize-tt";
            tooltip.className = "ytp-tooltip ytp-bottom";
            tooltip.style.position = "fixed";
            tooltip.style.zIndex = "10000";

            const wrapper = document.createElement("div");
            wrapper.className = "ytp-tooltip-text-wrapper ytp-tooltip-bottom-text";

            const tooltipText = document.createElement("span");
            tooltipText.className = "ytp-tooltip-text";
            tooltipText.id = "ytd-resize-tt-text";

            const tooltipKey = document.createElement("span");
            tooltipKey.id = "ytd-resize-tt-key";

            wrapper.appendChild(tooltipText);
            wrapper.appendChild(tooltipKey);
            tooltip.appendChild(wrapper);

            if (!document.getElementById("yt-sizer-tooltip-style")) {
                const style = document.createElement("style");
                style.id = "yt-sizer-tooltip-style";
                style.textContent = `
                #ytd-resize-tt .ytp-tooltip-text-wrapper {
                    display: flex;
                    align-items: center;
                }
                #ytd-resize-tt-key {
                    display: inline-flex;
                    align-items: center;
                    justify-content: center;
                    min-width: 15px;
                    height: 15px;
                    margin-left: 4px;
                    padding: 0 4px;
                    border: 1px solid rgba(255,255,255,0.30);
                    border-radius: 4px;
                    font-size: 12px;
                    font-weight: 500;
                    line-height: 15px;
                    box-sizing: border-box;
                    vertical-align: middle;
                }
            `;
                document.head.appendChild(style);
            }

            htmlPlayer.appendChild(tooltip);
            return tooltip;
        }
    }
    //==================================================================
    // Button setup
    //==================================================================
    function setButton(btn, path) {
        let pathData = {};
        let ariaLabel, titleText;
        if (getPref("yt-resize") !== true) {
            pathData.d = `M 13 17 L 5 9 L 5 17 Z
                          M 23 19
                          L 23 4.98 C 23 3.88 22.1 3 21 3
                          L 3 3 C 1.9 3 1 3.88 1 4.98
                          L 1 19 C 1 20.1 1.9 21 3 21
                          L 21 21 C 22.1 21 23 20.1 23 19
                          L 23 19 Z
                          M 21 19.02 L 3 19.02 L 3 4.97
                          L 21 4.97 L 21 19.02 L 21 19.02 Z`;
            ariaLabel = `Resize mode [${shortcutKey}]`;
            titleText = `Resize mode [${shortcutKey}]`;
        } else {
            pathData.d = `M 19 15 L 19 7 L 11 7 Z M 23 19
                          L 23 4.98 C 23 3.88 22.1 3 21 3
                          L 3 3 C 1.9 3 1 3.88 1 4.98
                          L 1 19 C 1 20.1 1.9 21 3 21
                          L 21 21 C 22.1 21 23 20.1 23 19
                          L 23 19 Z M 21 19.02
                          L 3 19.02 L 3 4.97
                          L 21 4.97 L 21 19.02 L 21 19.02 Z`;
            ariaLabel = `Default view [${shortcutKey}]`;
            titleText = `Default view [${shortcutKey}]`;
        }
        path.setAttribute("d", pathData.d);
        btn.setAttribute("aria-label", ariaLabel);
        btn.setAttribute("title", titleText);
    }

    function createButton(container) {
        const btn = document.createElement("button");
        const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
        svg.setAttribute("height", "24");
        svg.setAttribute("viewBox", "0 0 24 24");
        svg.setAttribute("width", "24");
        setButton(btn, path);
        path.setAttribute("fill", "white");
        svg.appendChild(path);
        btn.appendChild(svg);
        btn.classList.add("ytp-resize-button", "ytp-button");
        btn.setAttribute("id", "ytp-resize-button");
        btn.setAttribute("data-tooltip-target-id", "ytp-resize-button");
        container.insertBefore(btn, container.lastChild.previousSibling || container.lastChild);
        const showTooltip = (event) => {
            showResizeButtonTooltip(btn, ["mouseover", "focus"].includes(event.type));
        };
        btn.addEventListener("click", (e) => {
            e.stopPropagation();
            e.preventDefault();
            buttonScript();
        }, false);
        btn.addEventListener("mouseover", showTooltip);
        btn.addEventListener("mouseout", showTooltip);
        btn.addEventListener("focus", showTooltip);
        btn.addEventListener("blur", showTooltip);
    }

    function toggleStyle(id, cssTemplate) {
        const styleElement = document.getElementById(id);
        if (styleElement && document.head.contains(styleElement)) {
            document.head.removeChild(styleElement);
        } else {
            addCss(cssTemplate, id);
        }
    }

    function buttonScript() {
        toggleStyle(
            "max-player",
            `ytd-watch-flexy[flexy]:not([full-bleed-player][full-bleed-no-max-width-columns])
        #columns.ytd-watch-flexy { max-width: 100% !important; }`
        );
        toggleStyle(
            "small-player",
            `#primary.ytd-watch-flexy:not([theater]):not([fullscreen]) {
        max-width: ${getMaxWidth()}px !important; }`
        );
    }

    function shortScript() {
        const css = `#primary.ytd-watch-flexy:not([theater]):not([fullscreen]) { max-width: ${getMaxWidth()}px !important; }`;
        let splayer = document.getElementById("small-player");
        if (splayer && document.head.contains(splayer)) {
            splayer.textContent = css;
        } else {
            addCss(css, "small-player");
        }
    }
    //==================================================================
    // Keyboard / wheel handlers
    //==================================================================
    function handleKeydown(e) {
        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;
        const splayer = document.getElementById("small-player");
        if (e.key === shortcutKey.toLowerCase() || e.key === shortcutKey.toUpperCase()) {
            e.stopPropagation();
            e.preventDefault();
            buttonScript();
            return;
        }
        if (document.head.contains(splayer)) {
            if (e.key === "z") {
                e.stopPropagation();
                e.preventDefault();
                setMaxWidth(getMaxWidth() - 20);
                shortScript();
            } else if (e.key === "x") {
                e.stopPropagation();
                e.preventDefault();
                setMaxWidth(getMaxWidth() + 20);
                shortScript();
            }
        }
    }

    function handleWheel(e) {
        const splayer = document.getElementById("small-player");
        if (!document.head.contains(splayer)) return;
        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) return;
        e.stopPropagation();
        e.preventDefault();
        if (e.deltaY < 0) {
            setMaxWidth(getMaxWidth() + 20);
        } else if (e.deltaY > 0) {
            setMaxWidth(getMaxWidth() - 20);
        }
        shortScript();
    }
    //==================================================================
    // controlResize
    //==================================================================
    function addListenersOnce() {
        if (keyListenersAdded) return;
        document.addEventListener("keydown", handleKeydown, false);
        document.addEventListener("wheel", handleWheel, {
            passive: false
        });
        keyListenersAdded = true;
    }

    function controlResize() {
        const buttonExists = document.getElementById("ytp-resize-button");
        if (!buttonExists) {
            const container = document.querySelector(".ytp-right-controls-right") ||
                document.querySelector(".ytp-right-controls");
            if (container) {
                createButton(container);
                addListenersOnce();
            } else {
                waitElement(".ytp-right-controls-right, .ytp-right-controls").then((container) => {
                    if (!document.getElementById("ytp-resize-button")) {
                        createButton(container);
                    }
                    addListenersOnce();
                }).catch(() => {});
            }
        } else {
            setButton(buttonExists, buttonExists.querySelector("path"));
        }
    }
    //==================================================================
})();