- // ==UserScript==
- // @name YouTube Sub Feed Filter 2
- // @version 1.48
- // @description Set up filters for your sub feed
- // @author Callum Latham
- // @namespace https://greasyfork.org/users/696211-ctl2
- // @license MIT
- // @match *://www.youtube.com/*
- // @match *://youtube.com/*
- // @exclude *://www.youtube.com/embed/*
- // @exclude *://youtube.com/embed/*
- // @require https://update.greasyfork.org/scripts/446506/1537901/%24Config.js
- // @grant GM.setValue
- // @grant GM.getValue
- // @grant GM.deleteValue
- // ==/UserScript==
-
- /* global $Config */
-
- (() => {
- // 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',
- ];
-
- 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 videoTypeOptions = Object.values({
- ...VIDEO_TYPE_IDS.GROUPS,
- ...VIDEO_TYPE_IDS.INDIVIDUALS,
- });
-
- return {
- get: (_, configs) => Object.assign(...configs),
- children: [
- {
- label: 'Filters',
- get: (() => {
- const getRegex = ({children}) => children.length === 0 ?
- null :
- new RegExp(children.map(({value}) => `(${value})`).join('|'), REGEXP_FLAGS);
-
- return ({children}) => ({
- 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),
- })),
- });
- })(),
- 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,
- options: videoTypeOptions,
- },
- ],
- seed: {
- value: VIDEO_TYPE_IDS.GROUPS.NONE,
- options: videoTypeOptions,
- },
- childPredicate: (children) => {
- try {
- getVideoTypes(children);
- } catch ({message}) {
- return message;
- }
-
- return true;
- },
- },
- ],
- },
- },
- {
- label: 'Cutoffs',
- get: ({children}) => ({
- 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;
- }),
- }),
- 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],
- options: 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],
- options: 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],
- options: CUTOFF_VALUES,
- },
- {value: 0},
- ],
- },
- },
- ],
- },
- {
- label: 'Badges',
- get: ({children}) => ({badges: children.map(({value}) => BADGE_VALUES.indexOf(value))}),
- children: [
- {
- label: 'Verified',
- value: BADGE_VALUES[1],
- options: BADGE_VALUES,
- },
- {
- label: 'Official Artist',
- value: BADGE_VALUES[1],
- options: BADGE_VALUES,
- },
- ],
- },
- ],
- };
- })(),
- {
- headBase: '#c80000',
- headButtonExit: '#000000',
- borderHead: '#ffffff',
- borderTooltip: '#c80000',
- },
- {
- zIndex: 10000,
- scrollbarColor: 'initial',
- },
- );
-
- const KEY_IS_ACTIVE = 'YTSFF_IS_ACTIVE';
-
- // State
-
- let button;
-
- // Video element helpers
-
- function getSubPage() {
- return document.querySelector('.ytd-page-manager[page-subtype="subscriptions"]');
- }
-
- function getAllVideos() {
- const subPage = getSubPage();
-
- return [...subPage.querySelectorAll('#primary > ytd-rich-grid-renderer > #contents > :not(:first-child):not(ytd-continuation-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 isShorts(video) {
- return video.matches('[is-shorts] *');
- }
-
- function getMetadataLine(video) {
- return video.querySelector(isShorts(video) ? '.shortsLockupViewModelHostOutsideMetadata' : '#metadata-line');
- }
-
- function isScheduled(video) {
- if (isShorts(video)) {
- return false;
- }
-
- 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]: isShorts,
- [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') || child.matches('.shortsLockupViewModelHostMetadataSubhead'),
- );
- 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')) {
- 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')
- || video.querySelector('.shortsLockupViewModelHostOutsideMetadataTitle')
- ).innerText;
-
- for (const {'channels': channelRegex, 'videos': videoRegex, types} of filters) {
- if ((!channelRegex || channelName && channelRegex.test(channelName)) && (!videoRegex || 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'(element, doHide = true) {
- if (button.isActive) {
- hasReverted = false;
- }
-
- list.push({element, doHide, wasHidden: element.hidden});
-
- if (button.isActive) {
- 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);
- }
- },
- };
- })();
-
- const showList = (() => {
- const ATTRIBUTE = 'is-in-first-column';
-
- const list = [];
- const observers = [];
-
- let rowLength;
- let rowRemaining = 1;
- let hasReverted = true;
-
- function disconnectObservers() {
- for (const observer of observers) {
- observer.disconnect();
- }
-
- observers.length = 0;
- }
-
- function show(element, isFirst) {
- const act = isFirst ?
- () => element.setAttribute(ATTRIBUTE, true) :
- () => element.removeAttribute(ATTRIBUTE);
-
- act();
-
- const observer = new MutationObserver(() => {
- observer.disconnect();
-
- act();
-
- // Avoids observation cycle that I can't figure out the cause of
- window.setTimeout(() => {
- observer.observe(element, {attributeFilter: [ATTRIBUTE]});
- }, 0);
- });
-
- observer.observe(element, {attributeFilter: [ATTRIBUTE]});
-
- observers.push(observer);
- }
-
- return {
- 'add'(element) {
- if (list.length === 0) {
- rowLength = element.itemsPerRow ?? 3;
- }
-
- if (button.isActive) {
- hasReverted = false;
- }
-
- const isFirst = --rowRemaining === 0;
-
- if (isFirst) {
- rowRemaining = rowLength;
- }
-
- list.push({element, isFirst, wasFirst: element.hasAttribute(ATTRIBUTE)});
-
- if (button.isActive) {
- show(element, isFirst);
- }
- },
- 'revert'(doErase) {
- if (!hasReverted) {
- hasReverted = true;
-
- disconnectObservers();
-
- for (const {element, wasFirst} of list) {
- show(element, wasFirst);
- }
- }
-
- if (doErase) {
- list.length = 0;
- rowRemaining = 1;
- }
- },
- 'ensure'() {
- if (!hasReverted) {
- return;
- }
-
- hasReverted = false;
-
- for (const {element, isFirst} of list) {
- show(element, isFirst);
- }
- },
- 'lineFeed'() {
- rowRemaining = 1;
- },
- };
- })();
-
- async function hideVideo(element, config) {
- // video, else shorts container
- if (element.tagName === 'YTD-RICH-ITEM-RENDERER') {
- await loadVideo(element);
-
- if (shouldHide(config, element)) {
- hideList.add(element);
- } else {
- showList.add(element);
- }
-
- return;
- }
-
- let doHide = true;
-
- for (const video of element.querySelectorAll('ytd-rich-item-renderer')) {
- await loadVideo(video);
-
- if (shouldHide(config, video)) {
- hideList.add(video);
- } else {
- showList.add(video);
-
- doHide = false;
- }
- }
-
- if (doHide) {
- hideList.add(element);
- } else {
- showList.lineFeed();
- }
- }
-
- async function hideVideos(videos = getAllVideos()) {
- const config = $config.get();
-
- for (const video of videos) {
- await Promise.all([
- hideVideo(video, config),
- // Allow the page to update visually before moving on
- new Promise((resolve) => {
- window.setTimeout(resolve, 0);
- }),
- ]);
- }
- }
-
- // Helpers
-
- function resetConfig(fullReset = true) {
- hideList.revert(fullReset);
- showList.revert(fullReset);
- }
-
- function hideFromMutations(mutations) {
- const videos = [];
-
- for (const {addedNodes} of mutations) {
- for (const node of addedNodes) {
- switch (node.tagName) {
- case 'YTD-RICH-ITEM-RENDERER':
- case 'YTD-RICH-SECTION-RENDERER':
- videos.push(node);
- }
- }
- }
-
- hideVideos(videos);
- }
-
- 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');
- }
-
- // 🤷♀️
- const policy = trustedTypes?.createPolicy('policy', {createHTML: (string) => string}) ?? {createHTML: (string) => string};
-
- parentElement.appendChild(button);
-
- button.innerHTML = policy.createHTML(openerTemplate.innerHTML);
-
- button.querySelector('yt-button-shape').innerHTML = policy.createHTML(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);
-
- videoObserver.observe(
- document.querySelector('ytd-browse[page-subtype="subscriptions"]').querySelector('div#contents'),
- {childList: true},
- );
-
- hideVideos();
- });
-
- 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();
-
- hideVideos();
- }, 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();
- showList.ensure();
- } else {
- hideList.revert(false);
- showList.revert(false);
- }
- }
-
- async onLongClick() {
- await $config.edit();
-
- resetConfig();
-
- hideVideos();
- }
-
- onMouseDown(event) {
- if (event.button === 0) {
- new ClickHandler(this.element, this.toggleActive.bind(this), this.onLongClick.bind(this));
- }
- }
- }
-
- // Main
-
- (() => {
- const loadButton = async () => {
- if (button) {
- button.isDormant = false;
-
- hideVideos();
-
- 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 {
- 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-renderer ytd-rich-item-renderer ytd-rich-grid-media'),
- );
- };
-
- 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(true);
- showList.revert(true);
- }
- }
-
- document.body.addEventListener('yt-navigate-finish', onNavigate);
- })();
- })();