Greasy Fork is available in English.

YouTube Sub Feed Filter 2

Filters your YouTube subscriptions feed.

// ==UserScript==
// @name        YouTube Sub Feed Filter 2
// @version     1.18
// @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/1308867/%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',
        'PREMIERES': 'Premieres',
        'NONE': 'None',
    },
    'INDIVIDUALS': {
        'STREAMS_SCHEDULED': 'Scheduled Streams',
        'STREAMS_LIVE': 'Live Streams',
        'STREAMS_FINISHED': 'Finished Streams',
        'PREMIERES_SCHEDULED': 'Scheduled Premieres',
        'PREMIERES_LIVE': 'Live Premieres',
        'SHORTS': 'Shorts',
        'FUNDRAISERS': 'Fundraisers',
        'NORMAL': 'Basic Videos',
    },
};

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

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

const TITLE = 'YouTube Sub Feed Filter';

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.PREMIERES:
                register(VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERES_SCHEDULED);
                register(VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERES_LIVE);
                break;

            default:
                register(value);
        }
    }

    return registry;
}

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

            return true;
        };

        const videoTypePredicate = Object.values({
            ...VIDEO_TYPE_IDS.GROUPS,
            ...VIDEO_TYPE_IDS.INDIVIDUALS,
        });

        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': videoTypePredicate,
                                }],
                                'seed': {
                                    'value': VIDEO_TYPE_IDS.GROUPS.NONE,
                                    'predicate': videoTypePredicate,
                                },
                                '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';

// Removing row styling
(() => {
    const styleElement = document.createElement('style');
    document.head.appendChild(styleElement);
    const styleSheet = styleElement.sheet;

    const rules = [
        ['ytd-rich-grid-row #contents.ytd-rich-grid-row', [
            ['display', 'contents'],
        ]],
        ['ytd-rich-grid-row', [
            ['display', 'contents'],
        ]],
    ];

    for (let rule of rules) {
        styleSheet.insertRule(`${rule[0]}{${rule[1].map(([property, value]) => `${property}:${value} !important;`).join('')}}`);
    }
})();

// Video element helpers

function getSubPage() {
    return document.querySelector('.ytd-page-manager[page-subtype="subscriptions"]');
}

function getAllRows() {
    const subPage = getSubPage();

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

function getAllSections() {
    const subPage = getSubPage();

    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.PREMIERES_SCHEDULED](video);
}

function getUploadTimeNode(video) {
    const children = [...getMetadataLine(video).children].filter((child) => child.matches('.inline-metadata-item'));

    return children.length > 1 ? children[1] : null;
}

// 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 uploadTimeNode = getUploadTimeNode(video);

        return uploadTimeNode && firstWordEquals(uploadTimeNode, 'Streamed');
    },
    [VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERES_SCHEDULED]: (video) => {
        const metadataLine = getMetadataLine(video);

        return firstWordEquals(metadataLine, 'Premieres');
    },
    [VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERES_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 uploadTimeNode = getUploadTimeNode(video);

        return uploadTimeNode ? new RegExp('^\\d+ .+ ago$').test(uploadTimeNode.innerText) : false;
    },
    [VIDEO_TYPE_IDS.INDIVIDUALS.FUNDRAISERS]: (video) => {
        for (const badge of getVideoBadges(video)) {
            if (firstWordEquals(badge, 'Fundraiser')) {
                return true;
            }
        }

        return false;
    },
};

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')) {
                observer.disconnect();

                resolve();
            }
        };

        const observer = new MutationObserver(test);

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

        test();
    });
}

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 hideList = (() => {
    const list = [];

    let hasReverted = true;

    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) {
                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);
            }
        },
    };
})();

async function hideFromRows(config, doAct, groups = getAllRows()) {
    for (const group of groups) {
        const videos = getAllVideos(group);

        // 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);
            }

            resolve();
        })));

        // 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 (config, doAct, groups = getAllSections()) => {
        for (const group of groups) {
            const shownVideos = [];
            const backupVideos = [];

            for (const video of getAllVideos(group)) {
                await loadVideo(video);

                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, rows, sections, config = $config.get()) {
    return Promise.all([
        hideFromRows(config, doAct, rows),
        hideFromSections(config, doAct, sections),
    ]);
}

// Helpers

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

    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(), rows, sections);
}

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

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 {
    wasActive;
    isActive = false;
    isDormant = false;

    constructor() {
        this.element = (() => {
            const getSVG = () => {
                const svgNamespace = 'http://www.w3.org/2000/svg';

                const bottom = document.createElementNS(svgNamespace, 'path');

                bottom.setAttribute('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');

                const top = document.createElementNS(svgNamespace, 'rect');

                top.setAttribute('x', '13.95');
                top.setAttribute('width', '294');
                top.setAttribute('height', '45');

                const g = document.createElementNS(svgNamespace, 'g');

                g.appendChild(bottom);
                g.appendChild(top);

                const svg = document.createElementNS(svgNamespace, 'svg');

                svg.setAttribute('viewBox', '-50 -50 400 400');
                svg.setAttribute('focusable', 'false');
                svg.appendChild(g);

                return svg;
            };

            const getNewButton = () => {
                const {parentElement, 'children': [, openerTemplate]} = getButtonDock();
                const button = openerTemplate.cloneNode(false);

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

                parentElement.appendChild(button);

                button.innerHTML = openerTemplate.innerHTML;

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

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

                button.querySelector('yt-icon').appendChild(getSVG());

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

                return button;
            };

            return getNewButton();
        })();

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

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

            this.update();

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

            videoObserver.observe(
                document.querySelector('ytd-browse[page-subtype="subscriptions"]').querySelector('div#contents'),
                {childList: true},
            );

            hideAll(isActive);
        });

        let resizeCount = 0;

        window.addEventListener('resize', () => {
            const resizeId = ++resizeCount;

            this.forceInactive();

            const listener = ({detail}) => {
                // column size changed
                if (detail.actionName === 'yt-window-resized') {
                    window.setTimeout(() => {
                        if (resizeId !== resizeCount) {
                            return;
                        }

                        this.forceInactive(false);

                        // Don't bother re-running filters if the sub page isn't shown
                        if (this.isDormant) {
                            return;
                        }

                        resetConfig();

                        hideAll(this.isActive);
                    }, 1000);

                    document.body.removeEventListener('yt-action', listener);
                }
            };

            document.body.addEventListener('yt-action', listener);
        });
    }

    forceInactive(doForce = true) {
        if (doForce) {
            // if wasActive isn't undefined, forceInactive was already called
            if (this.wasActive === undefined) {
                // Saves a GM.getValue call later
                this.wasActive = this.isActive;
                this.isActive = false;
            }
        } else {
            this.isActive = this.wasActive;
            this.wasActive = undefined;
        }
    }

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

    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) {
            hideList.ensure();
        } else {
            hideList.revert(false);
        }
    }

    async onLongClick() {
        await $config.edit();

        resetConfig();

        hideAll(this.isActive);
    }

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

// Main

(() => {
    let button;

    const loadButton = async () => {
        if (button) {
            button.isDormant = false;

            hideAll(button.isActive);

            return;
        }

        try {
            await $config.ready;
        } catch (error) {
            if (!$config.reset) {
                throw error;
            }

            if (!window.confirm(`${error.message}\n\nWould you like to erase your data?`)) {
                return;
            }

            $config.reset();
        }

        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);
        }
    };

    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'),
        );
    };

    async function onNavigate({detail}) {
        if (detail.endpoint.browseEndpoint) {
            const {params, browseId} = detail.endpoint.browseEndpoint;

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

                if (button || isGridView()) {
                    loadButton();
                } else {
                    const listener = ({detail}) => {
                        if (detail.actionName === 'ytd-update-grid-state-action') {
                            if (isGridView()) {
                                loadButton();
                            }

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

                    emitter.addEventListener(event, listener);
                }

                return;
            }
        }

        // Handle navigation away from the sub feed
        if (button) {
            button.isDormant = true;

            hideList.revert();
        }
    }

    document.body.addEventListener('yt-navigate-finish', onNavigate);
})();