YouTube Sub Feed Filter 2

Filters your YouTube subscriptions feed.

目前为 2023-11-23 提交的版本。查看 最新版本

// ==UserScript==
// @name        YouTube Sub Feed Filter 2
// @version     1.9
// @description Filters your YouTube subscriptions feed.
// @author      Callum Latham
// @namespace   https://greasyfork.org/users/696211-ctl2
// @license     MIT
// @match       *://www.youtube.com/*
// @match       *://youtube.com/*
// @require     https://update.greasyfork.org/scripts/446506/1081062/%24Config.js
// @grant       GM.setValue
// @grant       GM.getValue
// @grant       GM.deleteValue
// ==/UserScript==

// Don't run in frames (e.g. stream chat frame)
if (window.parent !== window) {
    // noinspection JSAnnotator
    return;
}

// User config

const LONG_PRESS_TIME = 400;
const REGEXP_FLAGS = 'i';

// Dev config

const VIDEO_TYPE_IDS = {
    'GROUPS': {
        'ALL': 'All',
        'STREAMS': 'Streams',
        'PREMIERS': 'Premiers',
        'NONE': 'None'
    },
    'INDIVIDUALS': {
        'STREAMS_SCHEDULED': 'Scheduled Streams',
        'STREAMS_LIVE': 'Live Streams',
        'STREAMS_FINISHED': 'Finished Streams',
        'PREMIERS_SCHEDULED': 'Scheduled Premiers',
        'PREMIERS_LIVE': 'Live Premiers',
        'SHORTS': 'Shorts',
        'NORMAL': 'Basic Videos'
    }
};

const CUTOFF_VALUES = [
    'Minimum',
    'Maximum'
];

const BADGE_VALUES = [
    'Exclude',
    'Include',
    'Require'
];

const TITLE = 'YouTube Sub Feed Filter';

const $config = new $Config(
    'YTSFF_TREE',
    (() => {
        const regexPredicate = (value) => {
            try {
                RegExp(value);
            } catch (_) {
                return 'Value must be a valid regular expression.';
            }

            return true;
        };

        return {
            'children': [
                {
                    'label': 'Filters',
                    'children': [],
                    'seed': {
                        'label': 'Filter Name',
                        'value': '',
                        'children': [
                            {
                                'label': 'Channel Regex',
                                'children': [],
                                'seed': {
                                    'value': '^',
                                    'predicate': regexPredicate
                                }
                            },
                            {
                                'label': 'Video Regex',
                                'children': [],
                                'seed': {
                                    'value': '^',
                                    'predicate': regexPredicate
                                }
                            },
                            {
                                'label': 'Video Types',
                                'children': [{
                                    'value': VIDEO_TYPE_IDS.GROUPS.ALL,
                                    'predicate': Object.values(VIDEO_TYPE_IDS.GROUPS).concat(Object.values(VIDEO_TYPE_IDS.INDIVIDUALS))
                                }],
                                'seed': {
                                    'value': VIDEO_TYPE_IDS.GROUPS.NONE,
                                    'predicate': Object.values(VIDEO_TYPE_IDS.GROUPS).concat(Object.values(VIDEO_TYPE_IDS.INDIVIDUALS))
                                },
                                'childPredicate': (children) => {
                                    try {
                                        getVideoTypes(children);
                                    } catch ({message}) {
                                        return message;
                                    }

                                    return true;
                                }
                            }
                        ]
                    }
                },
                {
                    'label': 'Cutoffs',
                    'children': [
                        {
                            'label': 'Watched (%)',
                            'children': [],
                            'seed': {
                                'childPredicate': ([{'value': boundary}, {value}]) => {
                                    if (boundary === CUTOFF_VALUES[0]) {
                                        return value < 100 ? true : 'Minimum must be less than 100%';
                                    }

                                    return value > 0 ? true : 'Maximum must be greater than 0%';
                                },
                                'children': [
                                    {
                                        'value': CUTOFF_VALUES[1],
                                        'predicate': CUTOFF_VALUES
                                    },
                                    {
                                        'value': 100
                                    }
                                ]
                            }
                        },
                        {
                            'label': 'View Count',
                            'children': [],
                            'seed': {
                                'childPredicate': ([{'value': boundary}, {value}]) => {
                                    if (boundary === CUTOFF_VALUES[1]) {
                                        return value > 0 ? true : 'Maximum must be greater than 0';
                                    }

                                    return true;
                                },
                                'children': [
                                    {
                                        'value': CUTOFF_VALUES[0],
                                        'predicate': CUTOFF_VALUES
                                    },
                                    {
                                        'value': 0,
                                        'predicate': (value) => Math.floor(value) === value ? true : 'Value must be an integer'
                                    }
                                ]
                            }
                        },
                        {
                            'label': 'Duration (Minutes)',
                            'children': [],
                            'seed': {
                                'childPredicate': ([{'value': boundary}, {value}]) => {
                                    if (boundary === CUTOFF_VALUES[1]) {
                                        return value > 0 ? true : 'Maximum must be greater than 0';
                                    }

                                    return true;
                                },
                                'children': [
                                    {
                                        'value': CUTOFF_VALUES[0],
                                        'predicate': CUTOFF_VALUES
                                    },
                                    {
                                        'value': 0
                                    }
                                ]
                            }
                        }
                    ]
                },
                {
                    'label': 'Badges',
                    'children': [
                        {
                            'label': 'Verified',
                            'value': BADGE_VALUES[1],
                            'predicate': BADGE_VALUES
                        },
                        {
                            'label': 'Official Artist',
                            'value': BADGE_VALUES[1],
                            'predicate': BADGE_VALUES
                        }
                    ]
                }
            ]
        };
    })(),
    ([filters, cutoffs, badges]) => ({
        'filters': (() => {
            const getRegex = ({children}) => new RegExp(children.length === 0 ? '' :
                children.map(({value}) => `(${value})`).join('|'), REGEXP_FLAGS);

            return filters.children.map(({'children': [channel, video, type]}) => ({
                'channels': getRegex(channel),
                'videos': getRegex(video),
                'types': type.children.length === 0 ? Object.values(VIDEO_TYPE_IDS.INDIVIDUALS) : getVideoTypes(type.children)
            }));
        })(),
        'cutoffs': cutoffs.children.map(({children}) => {
            const boundaries = [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY];

            for (const {'children': [{'value': boundary}, {value}]} of children) {
                boundaries[boundary === CUTOFF_VALUES[0] ? 0 : 1] = value;
            }

            return boundaries;
        }),
        'badges': badges.children.map(({value}) => BADGE_VALUES.indexOf(value))
    }),
    TITLE,
    {
        'headBase': '#ff0000',
        'headButtonExit': '#000000',
        'borderHead': '#ffffff',
        'nodeBase': ['#222222', '#111111'],
        'borderTooltip': '#570000'
    },
    {'zIndex': 10000}
);

const KEY_IS_ACTIVE = 'YTSFF_IS_ACTIVE';

function getVideoTypes(children) {
    const registry = new Set();
    const register = (value) => {
        if (registry.has(value)) {
            throw new Error(`Overlap found at '${value}'.`);
        }

        registry.add(value);
    };

    for (const {value} of children) {
        switch (value) {
            case VIDEO_TYPE_IDS.GROUPS.ALL:
                Object.values(VIDEO_TYPE_IDS.INDIVIDUALS).forEach(register);
                break;

            case VIDEO_TYPE_IDS.GROUPS.STREAMS:
                register(VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_SCHEDULED);
                register(VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_LIVE);
                register(VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_FINISHED);
                break;

            case VIDEO_TYPE_IDS.GROUPS.PREMIERS:
                register(VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_SCHEDULED);
                register(VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_LIVE);
                break;

            default:
                register(value);
        }
    }

    return registry;
}

// Video element helpers

function getAllRows() {
    const subPage = document.querySelector('.ytd-page-manager[page-subtype="subscriptions"]');

    return subPage ? [...subPage.querySelectorAll('ytd-rich-grid-row')] : [];
}

function getAllSections() {
    const subPage = document.querySelector('.ytd-page-manager[page-subtype="subscriptions"]');

    return subPage ? [...subPage.querySelectorAll('ytd-rich-section-renderer:not(:first-child)')] : [];
}

function getAllVideos(row) {
    return [...row.querySelectorAll('ytd-rich-item-renderer')];
}

function firstWordEquals(element, word) {
    return element.innerText.split(' ')[0] === word;
}

function getVideoBadges(video) {
    return video.querySelectorAll('.video-badge');
}

function getChannelBadges(video) {
    const container = video.querySelector('ytd-badge-supported-renderer.ytd-channel-name');

    return container ? [...container.querySelectorAll('.badge')] : [];
}

function getMetadataLine(video) {
    return video.querySelector('#metadata-line');
}

function isScheduled(video) {
    return VIDEO_PREDICATES[VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_SCHEDULED](video) ||
        VIDEO_PREDICATES[VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_SCHEDULED](video);
}

function isLive(video) {
    return VIDEO_PREDICATES[VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_LIVE](video) ||
        VIDEO_PREDICATES[VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_LIVE](video);
}

// Config testers

const VIDEO_PREDICATES = {
    [VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_SCHEDULED]: (video) => {
        const metadataLine = getMetadataLine(video);

        return firstWordEquals(metadataLine, 'Scheduled');
    },
    [VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_LIVE]: (video) => {
        for (const badge of getVideoBadges(video)) {
            if (firstWordEquals(badge, 'LIVE')) {
                return true;
            }
        }

        return false;
    },
    [VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_FINISHED]: (video) => {
        const children = [...getMetadataLine(video).children].filter((child) => child.matches('.inline-metadata-item'));

        return children.length > 1 && firstWordEquals(children[1], 'Streamed');
    },
    [VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_SCHEDULED]: (video) => {
        const [schedule] = getMetadataLine(video).children;

        return firstWordEquals(schedule, 'Premieres');
    },
    [VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_LIVE]: (video) => {
        for (const badge of getVideoBadges(video)) {
            if (firstWordEquals(badge, 'PREMIERING') || firstWordEquals(badge, 'PREMIERE')) {
                return true;
            }
        }

        return false;
    },
    [VIDEO_TYPE_IDS.INDIVIDUALS.SHORTS]: (video) => {
        return video.querySelector('ytd-rich-grid-slim-media')?.isShort ?? false;
    },
    [VIDEO_TYPE_IDS.INDIVIDUALS.NORMAL]: (video) => {
        const [, {innerText}] = getMetadataLine(video).children;

        return new RegExp('^\\d+ .+ ago$').test(innerText);
    }
};

const CUTOFF_GETTERS = [
    // Watched %
    (video) => {
        const progressBar = video.querySelector('#progress');

        if (!progressBar) {
            return 0;
        }

        return Number.parseInt(progressBar.style.width.slice(0, -1));
    },
    // View count
    (video) => {
        if (isScheduled(video)) {
            return 0;
        }

        const {innerText} = [...getMetadataLine(video).children].find((child) => child.matches('.inline-metadata-item'));
        const [valueString] = innerText.split(' ');
        const lastChar = valueString.slice(-1);

        if (/\d/.test(lastChar)) {
            return Number.parseInt(valueString);
        }

        const valueNumber = Number.parseFloat(valueString.slice(0, -1));

        switch (lastChar) {
            case 'B':
                return valueNumber * 1000000000;
            case 'M':
                return valueNumber * 1000000;
            case 'K':
                return valueNumber * 1000;
        }

        return valueNumber;
    },
    // Duration (minutes)
    (video) => {
        const timeElement = video.querySelector('ytd-thumbnail-overlay-time-status-renderer');

        let minutes = 0;

        if (timeElement) {
            const timeParts = timeElement.innerText.split(':').map((_) => Number.parseInt(_));

            let timeValue = 1 / 60;

            for (let i = timeParts.length - 1; i >= 0; --i) {
                minutes += timeParts[i] * timeValue;

                timeValue *= 60;
            }
        }

        return Number.isNaN(minutes) ? 0 : minutes;
    }
];

const BADGE_PREDICATES = [
    // Verified
    (video) => getChannelBadges(video)
        .some((badge) => badge.classList.contains('badge-style-type-verified')),
    // Official Artist
    (video) => getChannelBadges(video)
        .some((badge) => badge.classList.contains('badge-style-type-verified-artist'))
];

// Hider functions

function loadVideo(video) {
    return new Promise((resolve) => {
        const test = () => {
            if (video.querySelector('#interaction.yt-icon-button')) {
                resolve();
            }
        };

        test();

        new MutationObserver(test)
            .observe(video, {
                'childList': true,
                'subtree': true,
                'attributes': true,
                'attributeOldValue': true
            });
    });
}

function shouldHide({filters, cutoffs, badges}, video) {
    for (let i = 0; i < BADGE_PREDICATES.length; ++i) {
        if (badges[i] !== 1 && Boolean(badges[i]) !== BADGE_PREDICATES[i](video)) {
            return true;
        }
    }

    for (let i = 0; i < CUTOFF_GETTERS.length; ++i) {
        const [lowerBound, upperBound] = cutoffs[i];
        const value = CUTOFF_GETTERS[i](video);

        if (value < lowerBound || value > upperBound) {
            return true;
        }
    }

    const channelName = video.querySelector('ytd-channel-name#channel-name')?.innerText;
    const videoName = video.querySelector('#video-title').innerText;

    for (const {'channels': channelRegex, 'videos': videoRegex, types} of filters) {
        if (
            (!channelName || channelRegex.test(channelName)) &&
            videoRegex.test(videoName)
        ) {
            for (const type of types) {
                if (VIDEO_PREDICATES[type](video)) {
                    return true;
                }
            }
        }
    }

    return false;
}

const moveList = (() => {
    const list = [];

    let hasReverted = false;

    return {
        'add'(doAct, element, destination) {
            if (doAct) {
                hasReverted = false;
            }

            list.push({element, destination, 'origin': element.parentElement});

            if (doAct) {
                destination.appendChild(element);
            }
        },
        'revert'(doErase) {
            if (hasReverted) {
                return;
            }

            hasReverted = true;

            for (let i = list.length - 1; i >= 0; --i) {
                const {origin, element} = list[i];

                origin.prepend(element);
            }

            if (doErase) {
                list.length = 0;
            }
        },
        'ensure'() {
            if (!hasReverted) {
                return;
            }

            hasReverted = false;

            for (const {element, destination} of list) {
                destination.appendChild(element);
            }
        }
    };
})();

const hideList = (() => {
    const list = [];

    let hasReverted = false;

    function hide(element, doHide) {
        element.hidden = false;

        if (doHide) {
            element.style.display = 'none';
        } else {
            element.style.removeProperty('display');
        }
    }

    return {
        'add'(doAct, element, doHide = true) {
            if (doAct) {
                hasReverted = false;
            }

            list.push({element, doHide, 'wasHidden': element.hidden});

            if (doAct) {
                hide(element, doHide);
            }
        },
        'revert'(doErase) {
            if (hasReverted) {
                return;
            }

            hasReverted = true;

            for (const {element, doHide, wasHidden} of list) {
                hide(element, !doHide);

                element.hidden = wasHidden;
            }

            if (doErase) {
                list.length = 0;
            }
        },
        'ensure'() {
            if (!hasReverted) {
                return;
            }

            hasReverted = false;

            for (const {element, doHide} of list) {
                hide(element, doHide);
            }
        }
    };
})();

let partialGroup;

const hideFromRows = (() => {
    return async (doAct, config, groups = getAllRows()) => {
        for (const group of groups) {
            const videos = getAllVideos(group);
            const [{itemsPerRow, parentElement}] = videos;

            let lossCount = 0;

            // Process all videos in the row in parallel
            await Promise.all(videos.map((video) => new Promise(async (resolve) => {
                await loadVideo(video);

                if (shouldHide(config, video)) {
                    hideList.add(doAct, video);

                    if (partialGroup) {
                        // Allows things to be put back in the right order
                        moveList.add(doAct, video, partialGroup.container);
                    }

                    ++lossCount;
                } else if (partialGroup) {
                    moveList.add(doAct, video, partialGroup.container);

                    if (--partialGroup.required <= 0) {
                        partialGroup = undefined;
                    }

                    ++lossCount;
                }

                resolve();
            })));

            if (!partialGroup) {
                if (lossCount > 0 && lossCount < itemsPerRow) {
                    partialGroup = {
                        'container': parentElement,
                        'required': lossCount
                    };
                }
            }

            // Allow the page to update visually before moving on to the next row
            await new Promise((resolve) => {
                window.setTimeout(resolve, 0);
            });
        }
    };
})();

const hideFromSections = (() => {
    return async (doAct, config, groups = getAllSections()) => {
        for (const group of groups) {
            const shownVideos = [];
            const backupVideos = [];

            for (const video of getAllVideos(group)) {
                if (video.hidden) {
                    if (!shouldHide(config, video)) {
                        backupVideos.push(video);
                    }
                } else {
                    shownVideos.push(video);
                }
            }

            let lossCount = 0;

            // Process all videos in the row in parallel
            await Promise.all(shownVideos.map((video) => new Promise(async (resolve) => {
                await loadVideo(video);

                if (shouldHide(config, video)) {
                    hideList.add(doAct, video);

                    if (backupVideos.length > 0) {
                        hideList.add(doAct, backupVideos.shift(), false);
                    } else {
                        lossCount++;
                    }
                }

                resolve();
            })));

            if (lossCount >= shownVideos.length) {
                hideList.add(doAct, group);
            }

            // Allow the page to update visually before moving on to the next row
            await new Promise((resolve) => {
                window.setTimeout(resolve, 0);
            });
        }
    };
})();

function hideAll(doAct = true, config = $config.get(), rows, sections) {
    return Promise.all([
        hideFromRows(doAct, config, rows),
        hideFromSections(doAct, config, sections)
    ]);
}

// Helpers

async function hideFromMutations(isActive, mutations) {
    const rows = [];
    const sections = [];

    if (isActive()) {
        hideList.ensure();

        moveList.ensure();
    }

    for (const {addedNodes} of mutations) {
        for (const node of addedNodes) {
            switch (node.tagName) {
                case 'YTD-RICH-GRID-ROW':
                    rows.push(node);
                    break;

                case 'YTD-RICH-SECTION-RENDERER':
                    sections.push(node);
            }
        }
    }

    hideAll(isActive(), $config.get(), rows, sections);
}

function resetConfig(fullReset = true) {
    hideList.revert(fullReset);

    moveList.revert(fullReset);

    if (fullReset) {
        partialGroup = undefined;
    }
}

function getButtonDock() {
    return document
        .querySelector('ytd-browse[page-subtype="subscriptions"]')
        .querySelector('#contents')
        .querySelector('#title-container')
        .querySelector('#top-level-buttons-computed');
}

// Button

class ClickHandler {
    constructor(button, onShortClick, onLongClick) {
        this.onShortClick = (function() {
            onShortClick();

            window.clearTimeout(this.longClickTimeout);

            window.removeEventListener('mouseup', this.onShortClick);
        }).bind(this);

        this.onLongClick = (function() {
            window.removeEventListener('mouseup', this.onShortClick);

            onLongClick();
        }).bind(this);

        this.longClickTimeout = window.setTimeout(this.onLongClick, LONG_PRESS_TIME);

        window.addEventListener('mouseup', this.onShortClick);
    }
}

class Button {
    isActive = false;

    constructor() {
        this.element = this.getNewButton();

        this.element.addEventListener('mousedown', this.onMouseDown.bind(this));

        GM.getValue(KEY_IS_ACTIVE, true).then((isActive) => {
            this.isActive = isActive;

            const videoObserver = new MutationObserver(hideFromMutations.bind(null, () => this.isActive));

            $config.init()
                .catch(({message}) => {
                    window.alert(message);
                })
                .then(() => {
                    videoObserver.observe(
                        document.querySelector('ytd-browse[page-subtype="subscriptions"]').querySelector('div#contents'),
                        {childList: true}
                    );

                    hideAll(isActive);
                });

            this.update();
        });
    }

    update() {
        if (this.isActive) {
            this.setButtonActive();
        }
    }

    addToDOM(button = this.element) {
        const {parentElement} = getButtonDock();

        parentElement.appendChild(button);
    }

    getNewButton() {
        const openerTemplate = getButtonDock().children[1];
        const button = openerTemplate.cloneNode(false);

        if (openerTemplate.innerText) {
            throw new Error('too early');
        }

        this.addToDOM(button);

        button.innerHTML = openerTemplate.innerHTML;

        button.querySelector('yt-button-shape').innerHTML = openerTemplate.querySelector('yt-button-shape').innerHTML;

        button.querySelector('a').removeAttribute('href');

        // TODO Build the svg via javascript
        button.querySelector('yt-icon').innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" focusable="false" viewBox="-50 -50 400 400"><g><path d="M128.25,175.6c1.7,1.8,2.7,4.1,2.7,6.6v139.7l60-51.3v-88.4c0-2.5,1-4.8,2.7-6.6L295.15,65H26.75L128.25,175.6z"/><rect x="13.95" y="0" width="294" height="45"/></g></svg>';

        button.querySelector('tp-yt-paper-tooltip').remove();

        return button;
    }

    hide() {
        this.element.style.display = 'none';
    }

    show() {
        this.element.parentElement.appendChild(this.element);
        this.element.style.removeProperty('display');
    }

    setButtonActive() {
        if (this.isActive) {
            this.element.querySelector('svg').style.setProperty('fill', 'var(--yt-spec-call-to-action)');
        } else {
            this.element.querySelector('svg').style.setProperty('fill', 'currentcolor');
        }
    }

    toggleActive() {
        this.isActive = !this.isActive;

        this.setButtonActive();

        GM.setValue(KEY_IS_ACTIVE, this.isActive);

        if (this.isActive) {
            moveList.ensure();
            hideList.ensure();
        } else {
            moveList.revert(false);
            hideList.revert(false);
        }
    }

    onLongClick() {
        $config.edit()
            .then(() => {
                if (this.isActive) {
                    resetConfig();

                    hideAll();
                }
            })
            .catch((error) => {
                console.error(error);

                if (window.confirm(
                    `[${TITLE}]` +
                    '\n\nYour config\'s structure is invalid.' +
                    '\nThis could be due to a script update or your data being corrupted.' +
                    '\n\nError Message:' +
                    `\n${error}` +
                    '\n\nWould you like to erase your data?'
                )) {
                    $config.reset();
                }
            });
    }

    async onMouseDown(event) {
        if (event.button === 0) {
            new ClickHandler(this.element, this.toggleActive.bind(this), this.onLongClick.bind(this));
        }
    }
}

// Main

(() => {
    let button;

    const loadButton = () => {
        if (!button) {
            try {
                getButtonDock();

                button = new Button();
            } catch (e) {
                const emitter = document.getElementById('page-manager');
                const bound = () => {
                    loadButton();

                    emitter.removeEventListener('yt-action', bound);
                };

                emitter.addEventListener('yt-action', bound);

                return;
            }
        } else if (button.isActive) {
            hideList.ensure();

            moveList.ensure();
        }

        button.show();
    };

    const isGridView = () => {
        return Boolean(
            document.querySelector('ytd-browse[page-subtype="subscriptions"]:not([hidden])') &&
            document.querySelector('ytd-browse > ytd-two-column-browse-results-renderer ytd-rich-grid-row ytd-rich-item-renderer ytd-rich-grid-media')
        );
    };

    const onNavigate = ({browseEndpoint}) => {
        if (browseEndpoint) {
            const {params, browseId} = browseEndpoint;

            if ((params === 'MAE%3D' || (!params && (!button || isGridView()))) && browseId === 'FEsubscriptions') {
                const emitter = document.querySelector('ytd-app');
                const event = 'yt-action';

                if (button || isGridView()) {
                    const listener = ({detail}) => {
                        if (detail.actionName === 'ytd-update-elements-per-row-action') {
                            loadButton();

                            emitter.removeEventListener(event, listener);
                        }
                    };

                    emitter.addEventListener(event, listener);
                } else {
                    const listener = ({detail}) => {
                        if (detail.actionName === 'ytd-update-grid-state-action') {
                            if (isGridView()) {
                                loadButton();
                            }

                            emitter.removeEventListener(event, listener);
                        }
                    };

                    emitter.addEventListener(event, listener);
                }

                return;
            }
        }

        if (button) {
            button.hide();

            if (button.isActive) {
                moveList.revert(false);
                hideList.revert(false);
            }
        }
    };

    (() => {
        document.body.addEventListener('yt-navigate-finish', ({detail}) => {
            onNavigate(detail.endpoint);
        });

        document.body.addEventListener('yt-action', ({detail}) => {
            if (detail.actionName === 'yt-store-grafted-ve-action') {
                moveList.revert(false);
                hideList.revert(false);
            }
        });
    })();
})();