MafiaBlackboxes

Hides the stupid FIIM spoilers by mercilessly covering them with black boxes. Tested in Chrome and Firefox on YouTube and Twitch.

// ==UserScript==
// @name         MafiaBlackboxes
// @namespace    http://tampermonkey.net/
// @version      1.7
// @description  Hides the stupid FIIM spoilers by mercilessly covering them with black boxes. Tested in Chrome and Firefox on YouTube and Twitch.
// @author       Kirill Khazan <[email protected]>
// @match        https://*.youtube.com/*
// @match        https://youtube.com/*
// @match        https://*.youtube.com/*
// @match        https://*.youtu.be/*
// @match        https://youtu.be/*
// @match        https://*.twitch.tv/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @license      MIT
// @grant        none
// ==/UserScript==

(function() {
    //    'use strict';
    var debounceData = {};
    var bbUninstallers = [];
    var tbUninstallers = [];
    var nAttempts = 0;

    function updateOnBoxEvents(uninstallers, fupdate) {
        // Function to update the black box when the video box is resized
        const video = document.querySelector('video');
        if(video == null) {
            return;
        }

        // Monitor changes in the video box size using ResizeObserver
        const resizeObserver = new ResizeObserver(() => {
            fupdate();
        });
        resizeObserver.observe(video);
        resizeObserver.observe(video.parentElement);
        uninstallers.push(() => resizeObserver.disconnect());

        const scrollHandler = () => {
            //console.log("win scrolled");
            fupdate();
        };
        window.addEventListener('scroll', scrollHandler);
        uninstallers.push(() => window.removeEventListener('scroll', scrollHandler));


        const intersectionObserver = new IntersectionObserver(fupdate, {
            root: null,
            threshold: buildThresholdList(),
        });
        intersectionObserver.observe(video);
        uninstallers.push(() => intersectionObserver.disconnect());
    }


    function videoDimensions(video) {
        // Ratio of the video's intrisic dimensions
        var videoRatio = video.videoWidth / video.videoHeight;
        // The width and height of the video element
        var width = video.offsetWidth, height = video.offsetHeight;
        // The ratio of the element's width to its height
        var elementRatio = width/height;
        // If the video element is short and wide
        if(elementRatio > videoRatio) width = height * videoRatio;
        // It must be tall and thin, or exactly equal to the original ratio
        else height = width / videoRatio;
        return {
            width: width,
            height: height
        };
    }

    function removeBlackBox(boxId) {
        const existingBlackBox = document.getElementById(boxId);
        if (existingBlackBox) {
            existingBlackBox.remove();
        }
    }

    function buildThresholdList() {
        let thresholds = [];
        let numSteps = 2000;

        for (let i = 1.0; i <= numSteps; i++) {
            let ratio = i / numSteps;
            thresholds.push(ratio);
        }

        thresholds.push(0);
        return thresholds;
    }

    function enableBlackBoxes() {
        console.log('enableBlackBoxes');
        bbEnabled = 1;

        function _updateBoxPosition(boxId, topRatio, leftRatio, widthRatio, heightRatio) {
            const blackBox = document.getElementById(boxId);
            if (!blackBox) {
                return;
            }

            console.log('updateBoxPosition');

            const video = document.querySelector('video'); // Selecting the video element on the page
            const videoBox = video.getBoundingClientRect();

            const actual = videoDimensions(video);
            const innerOffsetLeft = video.offsetLeft + (videoBox.width - actual.width) / 2;
            const innerOffsetTop = video.offsetTop + (videoBox.height - actual.height) / 2;

            //console.log(`VVV ${videoBox.top}, ${videoBox.left}, ${videoBox.height}, ${videoBox.width}, `);
            blackBox.style.position = 'absolute';
            blackBox.style.backgroundColor = 'black';
            blackBox.style.opacity = '1';

            blackBox.style.top = `${innerOffsetTop + actual.height * topRatio}px`;
            blackBox.style.left = `${innerOffsetLeft + actual.width * leftRatio}px`;
            blackBox.style.width = `${actual.width * widthRatio}px`;
            blackBox.style.height = `${actual.height * heightRatio}px`;

            blackBox.style.pointerEvents = 'none'; // To allow clicks to pass through
            blackBox.style.zIndex = '2'; // Ensure it's above other elements
        }

        var debounceData = {};

        function debounce(fn, boxId, ms = 0) {
            return function(...args) {
                const exec = function() {
                    delete debounceData[boxId];
                    fn.apply(this, args);
                };

                clearTimeout(debounceData[boxId]);
                debounceData[boxId] = setTimeout(() => exec(), ms);
            };
        }

        function updateBoxPosition(boxId, topRatio, leftRatio, widthRatio, heightRatio) {
            console.log(`debounce updateBoxPosition ${boxId}`);
            debounce(_updateBoxPosition, boxId, 30)(boxId, topRatio, leftRatio, widthRatio, heightRatio);
        }


        function createBlackBox(boxId, topRatio, leftRatio, widthRatio, heightRatio) {
            const video = document.querySelector('video'); // Selecting the video element on the page

            // Remove any existing black box
            removeBlackBox(boxId);

            var playerControls = document.querySelector('.ytp-chrome-bottom') || document.querySelector('.player-controls');
            playerControls.style.zIndex = '99'

            const blackBox = document.createElement('div');
            blackBox.id = boxId;

            updateBoxPosition(boxId, topRatio, leftRatio, widthRatio, heightRatio);
            //document.getElementsByClassName('video-stream html5-main-video')[0].parentElement.appendChild(blackBox);

            video.parentElement.appendChild(blackBox);
            //document.body.appendChild(blackBox);
        }

        function manageBlackBox(boxId, topRatio, leftRatio, widthRatio, heightRatio) {
            // Call the function to initially create the black box
            createBlackBox(boxId, topRatio, leftRatio, widthRatio, heightRatio);
            // Call the function to update the black box on video box resize
            // updateBlackBoxOnEvents();
            updateOnBoxEvents(bbUninstallers, () => updateBoxPosition(boxId, topRatio, leftRatio, widthRatio, heightRatio));
        }

        manageBlackBox('bottomBlackBox', 0.75, 0, 1, 0.25);
        manageBlackBox('topLeftBlackBox', 0, 0, 0.2, 0.16);
        manageBlackBox('topTopBlackBox', 0, 0, 0.65, 0.08);
        manageBlackBox('rightBlackBox', 0, 0.85, 0.15, 0.25);
    }



    function disableBlackBoxes() {
        bbEnabled = false;
        console.log('disableBlackBoxes');
        removeBlackBox('bottomBlackBox');
        removeBlackBox('topLeftBlackBox');
        removeBlackBox('topTopBlackBox');
        removeBlackBox('rightBlackBox');

        for (var boxId in Object.keys(debounceData)) {
            clearTimeout(debounceData[boxId]);
            delete debounceData[boxId];
        }

        bbUninstallers.forEach((u) => u());
        bbUninstallers = [];
    }


    var bbEnabled = false;
    function toggleBlackBoxes() {
        const button = document.getElementById('bbToggleButton');
        if(bbEnabled){
            disableBlackBoxes();
            //button.style.opacity = 0;
            button.textContent = 'Hide roles';
        } else {
            enableBlackBoxes();
            //button.style.opacity = 1;
            button.textContent = 'Show roles';
        }
    }

    function updateToggleButton() {
        console.log("updToggleButton");
        const video = document.querySelector('video');
        const button = document.getElementById('bbToggleButton');
        const videoBox = video.getBoundingClientRect();
        const actual = videoDimensions(video);
        const innerOffsetLeft = video.offsetLeft + (videoBox.width - actual.width) / 2;
        const innerOffsetTop = video.offsetTop + (videoBox.height - actual.height) / 2;

        //console.log(`VVV ${videoBox.top}, ${videoBox.left}, ${videoBox.height}, ${videoBox.width}, `);
        //console.log(`ZZZ ${innerOffsetTop}, ${actual.height}`);
        //console.log(`ZZZ ${video.offsetTop}, ${videoBox.height}, ${actual.height}`);
        //button.style.top = `${innerOffsetTop + 5}px`;
        if(video.offsetTop < 0) {
            button.style.top = `${videoBox.height * 0.05}px`;
            button.style.left = `px`;
        } else {
            button.style.top = `${innerOffsetTop + actual.height * 0.05}px`;
            button.style.left = `${innerOffsetLeft + 5}px`;
        }
    }

    function createToggleButton() {
        const video = document.querySelector('video');
        console.log(`YYY video is OK`);

        const button = document.createElement('button');
        button.id = 'bbToggleButton';
        button.textContent = 'Hide roles';
        button.style.cssText = `
            position: absolute;
            background-color: green;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            transition: opacity 0.2s ease-in-out;
            z-index: 200;
            opacity: 0;
            top: 5;
            left: 5;
            width: 200px;
            height: 80px;
            text-align: center;
        `;

        video.parentElement.appendChild(button);

        button.addEventListener('mouseover', () => {
            button.style.opacity = 1;
        });

        button.addEventListener('mouseout', () => {
            //button.style.opacity = bbEnabled ? 1 : 0;
            button.style.opacity = 0;
        });

        button.addEventListener('click', (event) => {
            event.stopPropagation();
            event.stopImmediatePropagation();
            event.preventDefault();
            toggleBlackBoxes();
            return false;
        });

        updateToggleButton();
    }

    function manageToggleButton() {
        const video = document.querySelector('video');
        if(video == null) {
            //console.log(`XXX video == null; ${document.body.childElementCount}`);
            if(nAttempts++ < 1000) {
                setTimeout(() => manageToggleButton(), 50);
            }
            return;
        }

        console.log(`YYY video is OK`);

        createToggleButton();
        updateOnBoxEvents(tbUninstallers, updateToggleButton);
    }

    function removeToggleButton() {
        const existingButton = document.getElementById('bbToggleButton');
        if (existingButton) {
            existingButton.remove();
        }
        tbUninstallers.forEach((u) => u());
        tbUninstallers = [];
    }

    const observeUrlChange = () => {
        let oldHref = document.location.href;
        const body = document.querySelector("body");
        const observer = new MutationObserver(mutations => {
            if (oldHref !== document.location.href) {
                oldHref = document.location.href;
                console.log('LOADDDDD');
                nAttempts = 0;
                disableBlackBoxes();
                removeToggleButton();
                manageToggleButton();
            }
        });
        observer.observe(body, { childList: true, subtree: true });
    };

    //window.onload = observeUrlChange;
    observeUrlChange();

    manageToggleButton();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址