YouTube Sub Feed Filter 2

Filters your YouTube subscriptions feed.

当前为 2022-09-17 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Sub Feed Filter 2
  3. // @version 1.4
  4. // @description Filters your YouTube subscriptions feed.
  5. // @author Callum Latham
  6. // @namespace https://greasyfork.org/users/696211-ctl2
  7. // @license MIT
  8. // @match *://www.youtube.com/*
  9. // @match *://youtube.com/*
  10. // @require https://greasyfork.org/scripts/446506-tree-frame-2/code/Tree%20Frame%202.js?version=1076104
  11. // @grant GM.setValue
  12. // @grant GM.getValue
  13. // @grant GM.deleteValue
  14. // ==/UserScript==
  15.  
  16. // Don't run in frames (e.g. stream chat frame)
  17. if (window.parent !== window) {
  18. // noinspection JSAnnotator
  19. return;
  20. }
  21.  
  22. // User config
  23.  
  24. const LONG_PRESS_TIME = 400;
  25. const REGEXP_FLAGS = 'i';
  26.  
  27. // Dev config
  28.  
  29. const VIDEO_TYPE_IDS = {
  30. 'GROUPS': {
  31. 'ALL': 'All',
  32. 'STREAMS': 'Streams',
  33. 'PREMIERS': 'Premiers',
  34. 'NONE': 'None'
  35. },
  36. 'INDIVIDUALS': {
  37. 'STREAMS_SCHEDULED': 'Scheduled Streams',
  38. 'STREAMS_LIVE': 'Live Streams',
  39. 'STREAMS_FINISHED': 'Finished Streams',
  40. 'PREMIERS_SCHEDULED': 'Scheduled Premiers',
  41. 'PREMIERS_LIVE': 'Live Premiers',
  42. 'SHORTS': 'Shorts',
  43. 'NORMAL': 'Basic Videos'
  44. }
  45. };
  46.  
  47. const FRAME_STYLE = {
  48. 'OUTER': {'zIndex': 10000},
  49. 'INNER': {
  50. 'headBase': '#ff0000',
  51. 'headButtonExit': '#000000',
  52. 'borderHead': '#ffffff',
  53. 'nodeBase': ['#222222', '#111111'],
  54. 'borderTooltip': '#570000'
  55. }
  56. };
  57. const TITLE = 'YouTube Sub Feed Filter';
  58. const KEY_TREE = 'YTSFF_TREE';
  59. const KEY_IS_ACTIVE = 'YTSFF_IS_ACTIVE';
  60.  
  61. function getVideoTypes(children) {
  62. const registry = new Set();
  63. const register = (value) => {
  64. if (registry.has(value)) {
  65. throw new Error(`Overlap found at '${value}'.`);
  66. }
  67.  
  68. registry.add(value);
  69. };
  70.  
  71. for (const {value} of children) {
  72. switch (value) {
  73. case VIDEO_TYPE_IDS.GROUPS.ALL:
  74. Object.values(VIDEO_TYPE_IDS.INDIVIDUALS).forEach(register);
  75. break;
  76.  
  77. case VIDEO_TYPE_IDS.GROUPS.STREAMS:
  78. register(VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_SCHEDULED);
  79. register(VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_LIVE);
  80. register(VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_FINISHED);
  81. break;
  82.  
  83. case VIDEO_TYPE_IDS.GROUPS.PREMIERS:
  84. register(VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_SCHEDULED);
  85. register(VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_LIVE);
  86. break;
  87.  
  88. default:
  89. register(value);
  90. }
  91. }
  92.  
  93. return registry;
  94. }
  95.  
  96. const CUTOFF_VALUES = [
  97. 'Minimum',
  98. 'Maximum'
  99. ];
  100.  
  101. const BADGE_VALUES = [
  102. 'Exclude',
  103. 'Include',
  104. 'Require'
  105. ];
  106.  
  107. const DEFAULT_TREE = (() => {
  108. const regexPredicate = (value) => {
  109. try {
  110. RegExp(value);
  111. } catch (_) {
  112. return 'Value must be a valid regular expression.';
  113. }
  114.  
  115. return true;
  116. };
  117.  
  118. return {
  119. 'children': [
  120. {
  121. 'label': 'Filters',
  122. 'children': [],
  123. 'seed': {
  124. 'label': 'Filter Name',
  125. 'value': '',
  126. 'children': [
  127. {
  128. 'label': 'Channel Regex',
  129. 'children': [],
  130. 'seed': {
  131. 'value': '^',
  132. 'predicate': regexPredicate
  133. }
  134. },
  135. {
  136. 'label': 'Video Regex',
  137. 'children': [],
  138. 'seed': {
  139. 'value': '^',
  140. 'predicate': regexPredicate
  141. }
  142. },
  143. {
  144. 'label': 'Video Types',
  145. 'children': [{
  146. 'value': VIDEO_TYPE_IDS.GROUPS.ALL,
  147. 'predicate': Object.values(VIDEO_TYPE_IDS.GROUPS).concat(Object.values(VIDEO_TYPE_IDS.INDIVIDUALS))
  148. }],
  149. 'seed': {
  150. 'value': VIDEO_TYPE_IDS.GROUPS.NONE,
  151. 'predicate': Object.values(VIDEO_TYPE_IDS.GROUPS).concat(Object.values(VIDEO_TYPE_IDS.INDIVIDUALS))
  152. },
  153. 'childPredicate': (children) => {
  154. try {
  155. getVideoTypes(children);
  156. } catch ({message}) {
  157. return message;
  158. }
  159.  
  160. return true;
  161. }
  162. }
  163. ]
  164. }
  165. },
  166. {
  167. 'label': 'Cutoffs',
  168. 'children': [
  169. {
  170. 'label': 'Watched (%)',
  171. 'children': [],
  172. 'seed': {
  173. 'childPredicate': ([{'value': boundary}, {value}]) => {
  174. if (boundary === CUTOFF_VALUES[0]) {
  175. return value < 100 ? true : 'Minimum must be less than 100%';
  176. }
  177.  
  178. return value > 0 ? true : 'Maximum must be greater than 0%';
  179. },
  180. 'children': [
  181. {
  182. 'value': CUTOFF_VALUES[1],
  183. 'predicate': CUTOFF_VALUES
  184. },
  185. {
  186. 'value': 100
  187. }
  188. ]
  189. }
  190. },
  191. {
  192. 'label': 'View Count',
  193. 'children': [],
  194. 'seed': {
  195. 'childPredicate': ([{'value': boundary}, {value}]) => {
  196. if (boundary === CUTOFF_VALUES[1]) {
  197. return value > 0 ? true : 'Maximum must be greater than 0';
  198. }
  199.  
  200. return true;
  201. },
  202. 'children': [
  203. {
  204. 'value': CUTOFF_VALUES[0],
  205. 'predicate': CUTOFF_VALUES
  206. },
  207. {
  208. 'value': 0,
  209. 'predicate': (value) => Math.floor(value) === value ? true : 'Value must be an integer'
  210. }
  211. ]
  212. }
  213. },
  214. {
  215. 'label': 'Duration (Minutes)',
  216. 'children': [],
  217. 'seed': {
  218. 'childPredicate': ([{'value': boundary}, {value}]) => {
  219. if (boundary === CUTOFF_VALUES[1]) {
  220. return value > 0 ? true : 'Maximum must be greater than 0';
  221. }
  222.  
  223. return true;
  224. },
  225. 'children': [
  226. {
  227. 'value': CUTOFF_VALUES[0],
  228. 'predicate': CUTOFF_VALUES
  229. },
  230. {
  231. 'value': 0
  232. }
  233. ]
  234. }
  235. }
  236. ]
  237. },
  238. {
  239. 'label': 'Badges',
  240. 'children': [
  241. {
  242. 'label': 'Verified',
  243. 'value': BADGE_VALUES[1],
  244. 'predicate': BADGE_VALUES
  245. },
  246. {
  247. 'label': 'Official Artist',
  248. 'value': BADGE_VALUES[1],
  249. 'predicate': BADGE_VALUES
  250. }
  251. ]
  252. }
  253. ]
  254. };
  255. })();
  256.  
  257. function getConfig([filters, cutoffs, badges]) {
  258. return {
  259. 'filters': (() => {
  260. const getRegex = ({children}) => new RegExp(children.length === 0 ? '' :
  261. children.map(({value}) => `(${value})`).join('|'), REGEXP_FLAGS);
  262.  
  263. return filters.children.map(({'children': [channel, video, type]}) => ({
  264. 'channels': getRegex(channel),
  265. 'videos': getRegex(video),
  266. 'types': type.children.length === 0 ? Object.values(VIDEO_TYPE_IDS.INDIVIDUALS) : getVideoTypes(type.children)
  267. }));
  268. })(),
  269. 'cutoffs': cutoffs.children.map(({children}) => {
  270. const boundaries = [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY];
  271.  
  272. for (const {'children': [{'value': boundary}, {value}]} of children) {
  273. boundaries[boundary === CUTOFF_VALUES[0] ? 0 : 1] = value;
  274. }
  275.  
  276. return boundaries;
  277. }),
  278. 'badges': badges.children.map(({value}) => BADGE_VALUES.indexOf(value))
  279. };
  280. }
  281.  
  282. // Video element helpers
  283.  
  284. function getAllSections() {
  285. const subPage = document.querySelector('.ytd-page-manager[page-subtype="subscriptions"]');
  286.  
  287. return subPage ? [...subPage.querySelectorAll('ytd-item-section-renderer')] : [];
  288. }
  289.  
  290. function getAllVideos(section) {
  291. return [...section.querySelectorAll('ytd-grid-video-renderer')];
  292. }
  293.  
  294. function firstWordEquals(element, word) {
  295. return element.innerText.split(' ')[0] === word;
  296. }
  297.  
  298. function getVideoBadges(video) {
  299. const container = video.querySelector('#video-badges');
  300.  
  301. return container ? [...container.querySelectorAll('.badge')] : [];
  302. }
  303.  
  304. function getChannelBadges(video) {
  305. const container = video.querySelector('ytd-badge-supported-renderer.ytd-channel-name');
  306.  
  307. return container ? [...container.querySelectorAll('.badge')] : [];
  308. }
  309.  
  310. function getMetadataLine(video) {
  311. return video.querySelector('#metadata-line');
  312. }
  313.  
  314. function isScheduled(video) {
  315. return VIDEO_PREDICATES[VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_SCHEDULED](video) ||
  316. VIDEO_PREDICATES[VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_SCHEDULED](video);
  317. }
  318.  
  319. function isLive(video) {
  320. return VIDEO_PREDICATES[VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_LIVE](video) ||
  321. VIDEO_PREDICATES[VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_LIVE](video);
  322. }
  323.  
  324. // Config testers
  325.  
  326. const VIDEO_PREDICATES = {
  327. [VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_SCHEDULED]: (video) => {
  328. const [schedule] = getMetadataLine(video).children;
  329.  
  330. return firstWordEquals(schedule, 'Scheduled');
  331. },
  332. [VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_LIVE]: (video) => {
  333. for (const badge of getVideoBadges(video)) {
  334. if (firstWordEquals(badge.querySelector('span.ytd-badge-supported-renderer'), 'LIVE')) {
  335. return true;
  336. }
  337. }
  338.  
  339. return false;
  340. },
  341. [VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_FINISHED]: (video) => {
  342. const {children} = getMetadataLine(video);
  343.  
  344. return children.length > 1 && firstWordEquals(children[1], 'Streamed');
  345. },
  346. [VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_SCHEDULED]: (video) => {
  347. const [schedule] = getMetadataLine(video).children;
  348.  
  349. return firstWordEquals(schedule, 'Premieres');
  350. },
  351. [VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_LIVE]: (video) => {
  352. for (const badge of getVideoBadges(video)) {
  353. const text = badge.querySelector('span.ytd-badge-supported-renderer');
  354.  
  355. if (firstWordEquals(text, 'PREMIERING') || firstWordEquals(text, 'PREMIERE')) {
  356. return true;
  357. }
  358. }
  359.  
  360. return false;
  361. },
  362. [VIDEO_TYPE_IDS.INDIVIDUALS.SHORTS]: (video) => {
  363. let icon = video.querySelector('[overlay-style]');
  364.  
  365. return icon && icon.getAttribute('overlay-style') === 'SHORTS';
  366. },
  367. [VIDEO_TYPE_IDS.INDIVIDUALS.NORMAL]: (video) => {
  368. const [, {innerText}] = getMetadataLine(video).children;
  369.  
  370. return new RegExp('^\\d+ .+ ago$').test(innerText);
  371. }
  372. };
  373.  
  374. const CUTOFF_GETTERS = [
  375. // Watched %
  376. (video) => {
  377. const progressBar = video.querySelector('#progress');
  378.  
  379. if (!progressBar) {
  380. return 0;
  381. }
  382.  
  383. return Number.parseInt(progressBar.style.width.slice(0, -1));
  384. },
  385. // View count
  386. (video) => {
  387. if (isScheduled(video)) {
  388. return 0;
  389. }
  390.  
  391. const [{innerText}] = getMetadataLine(video).children;
  392. const [valueString] = innerText.split(' ');
  393. const lastChar = valueString.slice(-1);
  394.  
  395. if (/\d/.test(lastChar)) {
  396. return Number.parseInt(valueString);
  397. }
  398.  
  399. const valueNumber = Number.parseFloat(valueString.slice(0, -1));
  400.  
  401. switch (lastChar) {
  402. case 'B':
  403. return valueNumber * 1000000000;
  404. case 'M':
  405. return valueNumber * 1000000;
  406. case 'K':
  407. return valueNumber * 1000;
  408. }
  409.  
  410. return valueNumber;
  411. },
  412. // Duration (minutes)
  413. (video) => {
  414. const timeElement = video.querySelector('ytd-thumbnail-overlay-time-status-renderer');
  415.  
  416. let minutes = 0;
  417.  
  418. if (timeElement) {
  419. const timeParts = timeElement.innerText.split(':').map((_) => Number.parseInt(_));
  420.  
  421. let timeValue = 1 / 60;
  422.  
  423. for (let i = timeParts.length - 1; i >= 0; --i) {
  424. minutes += timeParts[i] * timeValue;
  425.  
  426. timeValue *= 60;
  427. }
  428. }
  429.  
  430. return Number.isNaN(minutes) ? 0 : minutes;
  431. }
  432. ];
  433.  
  434. const BADGE_PREDICATES = [
  435. // Verified
  436. (video) => getChannelBadges(video)
  437. .some((badge) => badge.classList.contains('badge-style-type-verified')),
  438. // Official Artist
  439. (video) => getChannelBadges(video)
  440. .some((badge) => badge.classList.contains('badge-style-type-verified-artist'))
  441. ];
  442.  
  443. // Hider functions
  444.  
  445. function hideSection(section, doHide = true) {
  446. if (section.matches(':first-child')) {
  447. const title = section.querySelector('#title');
  448.  
  449. if (doHide) {
  450. section.style.height = '0';
  451. section.style.borderBottom = 'none';
  452. title.style.display = 'none';
  453. } else {
  454. section.style.removeProperty('height');
  455. section.style.removeProperty('borderBottom');
  456. title.style.removeProperty('display');
  457. }
  458. } else {
  459. if (doHide) {
  460. section.style.display = 'none';
  461. } else {
  462. section.style.removeProperty('display');
  463. }
  464. }
  465. }
  466.  
  467. function hideVideo(video, doHide = true) {
  468. if (doHide) {
  469. video.style.display = 'none';
  470. } else {
  471. video.style.removeProperty('display');
  472. }
  473. }
  474.  
  475. function loadVideo(video) {
  476. return new Promise((resolve) => {
  477. const test = () => {
  478. if (video.querySelector('#interaction.yt-icon-button')) {
  479. resolve();
  480. }
  481. };
  482.  
  483. test();
  484.  
  485. new MutationObserver(test)
  486. .observe(video, {
  487. 'childList ': true,
  488. 'subtree': true,
  489. 'attributes': true,
  490. 'attributeOldValue': true
  491. });
  492. });
  493. }
  494.  
  495. function shouldHide({filters, cutoffs, badges}, video) {
  496. for (let i = 0; i < BADGE_PREDICATES.length; ++i) {
  497. if (badges[i] !== 1 && Boolean(badges[i]) !== BADGE_PREDICATES[i](video)) {
  498. return true;
  499. }
  500. }
  501.  
  502. for (let i = 0; i < CUTOFF_GETTERS.length; ++i) {
  503. const [lowerBound, upperBound] = cutoffs[i];
  504. const value = CUTOFF_GETTERS[i](video);
  505.  
  506. if (value < lowerBound || value > upperBound) {
  507. return true;
  508. }
  509. }
  510.  
  511. // Separate the section's videos by hideability
  512. for (const {'channels': channelRegex, 'videos': videoRegex, types} of filters) {
  513. if (
  514. channelRegex.test(video.querySelector('a.yt-formatted-string').innerText) &&
  515. videoRegex.test(video.querySelector('a#video-title').innerText)
  516. ) {
  517. for (const type of types) {
  518. if (VIDEO_PREDICATES[type](video)) {
  519. return true;
  520. }
  521. }
  522. }
  523. }
  524.  
  525. return false;
  526. }
  527.  
  528. async function hideFromSections(config, sections = getAllSections()) {
  529. for (const section of sections) {
  530. if (section.matches('ytd-continuation-item-renderer')) {
  531. continue;
  532. }
  533.  
  534. const videos = getAllVideos(section);
  535.  
  536. let hiddenCount = 0;
  537.  
  538. // Process all videos in the section in parallel
  539. await Promise.all(videos.map((video) => new Promise(async (resolve) => {
  540. await loadVideo(video);
  541.  
  542. if (shouldHide(config, video)) {
  543. hideVideo(video);
  544.  
  545. hiddenCount++;
  546. }
  547.  
  548. resolve();
  549. })));
  550.  
  551. // Hide hideable videos
  552. if (hiddenCount === videos.length) {
  553. hideSection(section);
  554. }
  555.  
  556. // Allow the page to update before moving on to the next section
  557. await new Promise((resolve) => {
  558. window.setTimeout(resolve, 0);
  559. });
  560. }
  561. }
  562.  
  563. async function hideFromMutations(mutations) {
  564. const sections = [];
  565.  
  566. for (const {addedNodes} of mutations) {
  567. for (const section of addedNodes) {
  568. sections.push(section);
  569. }
  570. }
  571.  
  572. hideFromSections(getConfig(await getForest(KEY_TREE, DEFAULT_TREE)), sections);
  573. }
  574.  
  575. // Helpers
  576.  
  577. function resetConfig() {
  578. for (const section of getAllSections()) {
  579. hideSection(section, false);
  580.  
  581. for (const video of getAllVideos(section)) {
  582. hideVideo(video, false);
  583. }
  584. }
  585. }
  586.  
  587. function getButtonDock() {
  588. return document
  589. .querySelector('ytd-browse[page-subtype="subscriptions"]')
  590. .querySelector('#title-container')
  591. .querySelector('#top-level-buttons-computed');
  592. }
  593.  
  594. // Button
  595.  
  596. class ClickHandler {
  597. constructor(button, onShortClick, onLongClick) {
  598. this.onShortClick = (function() {
  599. onShortClick();
  600.  
  601. window.clearTimeout(this.longClickTimeout);
  602.  
  603. window.removeEventListener('mouseup', this.onShortClick);
  604. }).bind(this);
  605.  
  606. this.onLongClick = (function() {
  607. window.removeEventListener('mouseup', this.onShortClick);
  608.  
  609. onLongClick();
  610. }).bind(this);
  611.  
  612. this.longClickTimeout = window.setTimeout(this.onLongClick, LONG_PRESS_TIME);
  613.  
  614. window.addEventListener('mouseup', this.onShortClick);
  615. }
  616. }
  617.  
  618. class Button {
  619. constructor(pageManager) {
  620. this.pageManager = pageManager;
  621. this.element = this.getNewButton();
  622.  
  623. this.element.addEventListener('mousedown', this.onMouseDown.bind(this));
  624.  
  625. GM.getValue(KEY_IS_ACTIVE, true).then((isActive) => {
  626. this.isActive = isActive;
  627.  
  628. this.update();
  629. });
  630. }
  631.  
  632. update() {
  633. if (this.isActive) {
  634. this.setButtonActive();
  635.  
  636. this.pageManager.start();
  637. }
  638. }
  639.  
  640. addToDOM(button = this.element) {
  641. const {parentElement} = getButtonDock();
  642.  
  643. parentElement.appendChild(button);
  644. }
  645.  
  646. getNewButton() {
  647. const openerTemplate = getButtonDock().children[1];
  648. const button = openerTemplate.cloneNode(false);
  649.  
  650. this.addToDOM(button);
  651.  
  652. button.innerHTML = openerTemplate.innerHTML;
  653.  
  654. button.querySelector('button').innerHTML = openerTemplate.querySelector('button').innerHTML;
  655.  
  656. button.querySelector('a').removeAttribute('href');
  657.  
  658. // TODO Build the svg via javascript
  659. 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>`;
  660.  
  661. return button;
  662. }
  663.  
  664. hide() {
  665. this.element.style.display = 'none';
  666. }
  667.  
  668. show() {
  669. this.element.parentElement.appendChild(this.element);
  670. this.element.style.removeProperty('display');
  671. }
  672.  
  673. setButtonActive() {
  674. if (this.isActive) {
  675. this.element.classList.add('style-blue-text');
  676. this.element.classList.remove('style-opacity');
  677. } else {
  678. this.element.classList.add('style-opacity');
  679. this.element.classList.remove('style-blue-text');
  680. }
  681. }
  682.  
  683. toggleActive() {
  684. this.isActive = !this.isActive;
  685.  
  686. this.setButtonActive();
  687.  
  688. GM.setValue(KEY_IS_ACTIVE, this.isActive);
  689.  
  690. if (this.isActive) {
  691. this.pageManager.start();
  692. } else {
  693. this.pageManager.stop();
  694. }
  695. }
  696.  
  697. onLongClick() {
  698. editForest(KEY_TREE, DEFAULT_TREE, TITLE, FRAME_STYLE.INNER, FRAME_STYLE.OUTER)
  699. .then((forest) => {
  700. if (this.isActive) {
  701. resetConfig();
  702.  
  703. // Hide filtered videos
  704. hideFromSections(getConfig(forest));
  705. }
  706. })
  707. .catch((error) => {
  708. console.error(error);
  709.  
  710. if (window.confirm(
  711. `[${TITLE}]` +
  712. '\n\nYour config\'s structure is invalid.' +
  713. '\nThis could be due to a script update or your data being corrupted.' +
  714. '\n\nError Message:' +
  715. `\n${error}` +
  716. '\n\nWould you like to erase your data?'
  717. )) {
  718. GM.deleteValue(KEY_TREE);
  719. }
  720. });
  721. }
  722.  
  723. async onMouseDown(event) {
  724. if (event.button === 0) {
  725. new ClickHandler(this.element, this.toggleActive.bind(this), this.onLongClick.bind(this));
  726. }
  727. }
  728. }
  729.  
  730. // Page load/navigation handler
  731.  
  732. class PageManager {
  733. constructor() {
  734. this.videoObserver = new MutationObserver(hideFromMutations);
  735.  
  736. document
  737. .querySelector('ytd-app')
  738. .addEventListener('yt-navigate-finish', ({detail}) => {
  739. this.onNavigate(detail);
  740. });
  741.  
  742. document
  743. .body
  744. .addEventListener('popstate', ({state}) => {
  745. this.onNavigate(state);
  746. });
  747.  
  748. const canDock = () => this.isSubPage() && this.isGridView();
  749.  
  750. if (canDock()) {
  751. this.loadButton();
  752. }
  753.  
  754. const emitter = document.getElementById('page-manager');
  755. const event = 'yt-action';
  756. const onEvent = ({detail}) => {
  757. if (detail.actionName === 'ytd-update-grid-state-action') {
  758. if (canDock()) {
  759. this.loadButton();
  760. }
  761.  
  762. emitter.removeEventListener(event, onEvent);
  763. }
  764. };
  765.  
  766. emitter.addEventListener(event, onEvent);
  767. }
  768.  
  769. loadButton() {
  770. if (!this.button) {
  771. this.button = new Button(this);
  772. } else {
  773. this.button.update();
  774. }
  775.  
  776. this.button.show();
  777. }
  778.  
  779. start() {
  780. getForest(KEY_TREE, DEFAULT_TREE).then(forest => {
  781. // Call hide function when new videos are loaded
  782. this.videoObserver.observe(
  783. document.querySelector('ytd-browse[page-subtype="subscriptions"]').querySelector('div#contents'),
  784. {childList: true}
  785. );
  786.  
  787. try {
  788. hideFromSections(getConfig(forest));
  789. } catch (error) {
  790. console.error(error);
  791.  
  792. window.alert(
  793. `[${TITLE}]` +
  794. '\n\nUnable to execute filter; Expected config structure may have changed.' +
  795. '\nTry opening and closing the config editor to update your data\'s structure.'
  796. );
  797. }
  798. });
  799. }
  800.  
  801. stop() {
  802. this.videoObserver.disconnect();
  803.  
  804. resetConfig();
  805. }
  806.  
  807. isSubPage() {
  808. return new RegExp('^.*youtube.com/feed/subscriptions(\\?flow=1|\\?pbjreload=\\d+)?$').test(document.URL);
  809. }
  810.  
  811. isGridView() {
  812. return document.querySelector('ytd-item-section-renderer:not([hidden]) ytd-expanded-shelf-contents-renderer') === null;
  813. }
  814.  
  815. onNavigate({endpoint}) {
  816. if (endpoint.browseEndpoint) {
  817. const {params, browseId} = endpoint.browseEndpoint;
  818.  
  819. if ((params === 'MAE%3D' || (!params && this.isGridView())) && browseId === 'FEsubscriptions') {
  820. this.loadButton();
  821. } else {
  822. if (this.button) {
  823. this.button.hide();
  824. }
  825.  
  826. this.stop();
  827. }
  828. }
  829. }
  830. }
  831.  
  832. // Main
  833.  
  834. window.addEventListener('load', () => {
  835. new PageManager();
  836. });