YouTube Sub Feed Filter 2

Filters your YouTube subscriptions feed.

当前为 2024-06-27 提交的版本,查看 最新版本

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