YouTube Sub Feed Filter 2

Filters your YouTube subscriptions feed.

目前为 2024-06-30 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Sub Feed Filter 2
  3. // @version 1.21
  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/1402875/%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. {
  268. zIndex: 10000,
  269. scrollbarColor: 'initial',
  270. },
  271. );
  272.  
  273. const KEY_IS_ACTIVE = 'YTSFF_IS_ACTIVE';
  274.  
  275. // Removing row styling
  276. (() => {
  277. const styleElement = document.createElement('style');
  278. document.head.appendChild(styleElement);
  279. const styleSheet = styleElement.sheet;
  280. const rules = [
  281. ['ytd-rich-grid-row #contents.ytd-rich-grid-row', [['display', 'contents']]],
  282. ['ytd-rich-grid-row', [['display', 'contents']]],
  283. ];
  284. for (let rule of rules) {
  285. styleSheet.insertRule(`${rule[0]}{${rule[1].map(([property, value]) => `${property}:${value} !important;`).join('')}}`);
  286. }
  287. })();
  288.  
  289. // Video element helpers
  290.  
  291. function getSubPage() {
  292. return document.querySelector('.ytd-page-manager[page-subtype="subscriptions"]');
  293. }
  294.  
  295. function getAllRows() {
  296. const subPage = getSubPage();
  297. return subPage ? [...subPage.querySelectorAll('ytd-rich-grid-row')] : [];
  298. }
  299.  
  300. function getAllSections() {
  301. const subPage = getSubPage();
  302. return subPage ? [...subPage.querySelectorAll('ytd-rich-section-renderer:not(:first-child)')] : [];
  303. }
  304.  
  305. function getAllVideos(row) {
  306. return [...row.querySelectorAll('ytd-rich-item-renderer')];
  307. }
  308.  
  309. function firstWordEquals(element, word) {
  310. return element.innerText.split(' ')[0] === word;
  311. }
  312.  
  313. function getVideoBadges(video) {
  314. return video.querySelectorAll('.video-badge');
  315. }
  316.  
  317. function getChannelBadges(video) {
  318. const container = video.querySelector('ytd-badge-supported-renderer.ytd-channel-name');
  319. return container ? [...container.querySelectorAll('.badge')] : [];
  320. }
  321.  
  322. function getMetadataLine(video) {
  323. return video.querySelector('#metadata-line');
  324. }
  325.  
  326. function isScheduled(video) {
  327. return VIDEO_PREDICATES[VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_SCHEDULED](video)
  328. || VIDEO_PREDICATES[VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERES_SCHEDULED](video);
  329. }
  330.  
  331. function getUploadTimeNode(video) {
  332. const children = [...getMetadataLine(video).children].filter((child) => child.matches('.inline-metadata-item'));
  333. return children.length > 1 ? children[1] : null;
  334. }
  335.  
  336. // Config testers
  337.  
  338. const VIDEO_PREDICATES = {
  339. [VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_SCHEDULED]: (video) => {
  340. const metadataLine = getMetadataLine(video);
  341. return firstWordEquals(metadataLine, 'Scheduled');
  342. },
  343. [VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_LIVE]: (video) => {
  344. for (const badge of getVideoBadges(video)) {
  345. if (firstWordEquals(badge, 'LIVE')) {
  346. return true;
  347. }
  348. }
  349. return false;
  350. },
  351. [VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_FINISHED]: (video) => {
  352. const uploadTimeNode = getUploadTimeNode(video);
  353. return uploadTimeNode && firstWordEquals(uploadTimeNode, 'Streamed');
  354. },
  355. [VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERES_SCHEDULED]: (video) => {
  356. const metadataLine = getMetadataLine(video);
  357. return firstWordEquals(metadataLine, 'Premieres');
  358. },
  359. [VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERES_LIVE]: (video) => {
  360. for (const badge of getVideoBadges(video)) {
  361. if (firstWordEquals(badge, 'PREMIERING') || firstWordEquals(badge, 'PREMIERE')) {
  362. return true;
  363. }
  364. }
  365. return false;
  366. },
  367. [VIDEO_TYPE_IDS.INDIVIDUALS.SHORTS]: (video) => {
  368. return video.querySelector('ytd-rich-grid-slim-media')?.isShort ?? false;
  369. },
  370. [VIDEO_TYPE_IDS.INDIVIDUALS.NORMAL]: (video) => {
  371. const uploadTimeNode = getUploadTimeNode(video);
  372. return uploadTimeNode ? new RegExp('^\\d+ .+ ago$').test(uploadTimeNode.innerText) : false;
  373. },
  374. [VIDEO_TYPE_IDS.INDIVIDUALS.FUNDRAISERS]: (video) => {
  375. for (const badge of getVideoBadges(video)) {
  376. if (firstWordEquals(badge, 'Fundraiser')) {
  377. return true;
  378. }
  379. }
  380. return false;
  381. },
  382. };
  383.  
  384. const CUTOFF_GETTERS = [
  385. // Watched %
  386. (video) => {
  387. const progressBar = video.querySelector('#progress');
  388. if (!progressBar) {
  389. return 0;
  390. }
  391. return Number.parseInt(progressBar.style.width.slice(0, -1));
  392. },
  393. // View count
  394. (video) => {
  395. if (isScheduled(video)) {
  396. return 0;
  397. }
  398. const {innerText} = [...getMetadataLine(video).children].find((child) => child.matches('.inline-metadata-item'));
  399. const [valueString] = innerText.split(' ');
  400. const lastChar = valueString.slice(-1);
  401. if (/\d/.test(lastChar)) {
  402. return Number.parseInt(valueString);
  403. }
  404. const valueNumber = Number.parseFloat(valueString.slice(0, -1));
  405. switch (lastChar) {
  406. case 'B':
  407. return valueNumber * 1000000000;
  408. case 'M':
  409. return valueNumber * 1000000;
  410. case 'K':
  411. return valueNumber * 1000;
  412. }
  413. return valueNumber;
  414. },
  415. // Duration (minutes)
  416. (video) => {
  417. const timeElement = video.querySelector('ytd-thumbnail-overlay-time-status-renderer');
  418. let minutes = 0;
  419. if (timeElement) {
  420. const timeParts = timeElement.innerText.split(':').map((_) => Number.parseInt(_));
  421. let timeValue = 1 / 60;
  422. for (let i = timeParts.length - 1; i >= 0; --i) {
  423. minutes += timeParts[i] * timeValue;
  424. timeValue *= 60;
  425. }
  426. }
  427. return Number.isNaN(minutes) ? 0 : minutes;
  428. },
  429. ];
  430.  
  431. const BADGE_PREDICATES = [
  432. // Verified
  433. (video) => getChannelBadges(video)
  434. .some((badge) => badge.classList.contains('badge-style-type-verified')),
  435. // Official Artist
  436. (video) => getChannelBadges(video)
  437. .some((badge) => badge.classList.contains('badge-style-type-verified-artist')),
  438. ];
  439.  
  440. // Hider functions
  441.  
  442. function loadVideo(video) {
  443. return new Promise((resolve) => {
  444. const test = () => {
  445. if (video.querySelector('#interaction.yt-icon-button')) {
  446. observer.disconnect();
  447. resolve();
  448. }
  449. };
  450. const observer = new MutationObserver(test);
  451. observer.observe(video, {
  452. childList: true,
  453. subtree: true,
  454. attributes: true,
  455. attributeOldValue: true,
  456. });
  457. test();
  458. });
  459. }
  460.  
  461. function shouldHide({filters, cutoffs, badges}, video) {
  462. for (let i = 0; i < BADGE_PREDICATES.length; ++i) {
  463. if (badges[i] !== 1 && Boolean(badges[i]) !== BADGE_PREDICATES[i](video)) {
  464. return true;
  465. }
  466. }
  467. for (let i = 0; i < CUTOFF_GETTERS.length; ++i) {
  468. const [lowerBound, upperBound] = cutoffs[i];
  469. const value = CUTOFF_GETTERS[i](video);
  470. if (value < lowerBound || value > upperBound) {
  471. return true;
  472. }
  473. }
  474. const channelName = video.querySelector('ytd-channel-name#channel-name')?.innerText;
  475. const videoName = video.querySelector('#video-title').innerText;
  476. for (const {'channels': channelRegex, 'videos': videoRegex, types} of filters) {
  477. if (
  478. (!channelName || channelRegex.test(channelName))
  479. && videoRegex.test(videoName)
  480. ) {
  481. for (const type of types) {
  482. if (VIDEO_PREDICATES[type](video)) {
  483. return true;
  484. }
  485. }
  486. }
  487. }
  488. return false;
  489. }
  490.  
  491. const hideList = (() => {
  492. const list = [];
  493. let hasReverted = true;
  494. function hide(element, doHide) {
  495. element.hidden = false;
  496. if (doHide) {
  497. element.style.display = 'none';
  498. } else {
  499. element.style.removeProperty('display');
  500. }
  501. }
  502. return {
  503. 'add'(doAct, element, doHide = true) {
  504. if (doAct) {
  505. hasReverted = false;
  506. }
  507. list.push({element, doHide, wasHidden: element.hidden});
  508. if (doAct) {
  509. hide(element, doHide);
  510. }
  511. },
  512. 'revert'(doErase) {
  513. if (!hasReverted) {
  514. hasReverted = true;
  515. for (const {element, doHide, wasHidden} of list) {
  516. hide(element, !doHide);
  517. element.hidden = wasHidden;
  518. }
  519. }
  520. if (doErase) {
  521. list.length = 0;
  522. }
  523. },
  524. 'ensure'() {
  525. if (!hasReverted) {
  526. return;
  527. }
  528. hasReverted = false;
  529. for (const {element, doHide} of list) {
  530. hide(element, doHide);
  531. }
  532. },
  533. };
  534. })();
  535.  
  536. async function hideFromRows(config, doAct, groups = getAllRows()) {
  537. for (const group of groups) {
  538. const videos = getAllVideos(group);
  539. // Process all videos in the row in parallel
  540. await Promise.all(videos.map((video) => new Promise(async (resolve) => {
  541. await loadVideo(video);
  542. if (shouldHide(config, video)) {
  543. hideList.add(doAct, video);
  544. }
  545. resolve();
  546. })));
  547. // Allow the page to update visually before moving on to the next row
  548. await new Promise((resolve) => {
  549. window.setTimeout(resolve, 0);
  550. });
  551. }
  552. }
  553.  
  554. const hideFromSections = (() => {
  555. return async (config, doAct, groups = getAllSections()) => {
  556. for (const group of groups) {
  557. const shownVideos = [];
  558. const backupVideos = [];
  559. for (const video of getAllVideos(group)) {
  560. await loadVideo(video);
  561. if (video.hidden) {
  562. if (!shouldHide(config, video)) {
  563. backupVideos.push(video);
  564. }
  565. } else {
  566. shownVideos.push(video);
  567. }
  568. }
  569. let lossCount = 0;
  570. // Process all videos in the row in parallel
  571. await Promise.all(shownVideos.map((video) => new Promise(async (resolve) => {
  572. await loadVideo(video);
  573. if (shouldHide(config, video)) {
  574. hideList.add(doAct, video);
  575. if (backupVideos.length > 0) {
  576. hideList.add(doAct, backupVideos.shift(), false);
  577. } else {
  578. lossCount++;
  579. }
  580. }
  581. resolve();
  582. })));
  583. if (lossCount >= shownVideos.length) {
  584. hideList.add(doAct, group);
  585. }
  586. // Allow the page to update visually before moving on to the next row
  587. await new Promise((resolve) => {
  588. window.setTimeout(resolve, 0);
  589. });
  590. }
  591. };
  592. })();
  593.  
  594. function hideAll(doAct = true, rows, sections, config = $config.get()) {
  595. return Promise.all([
  596. hideFromRows(config, doAct, rows),
  597. hideFromSections(config, doAct, sections),
  598. ]);
  599. }
  600.  
  601. // Helpers
  602.  
  603. function hideFromMutations(isActive, mutations) {
  604. const rows = [];
  605. const sections = [];
  606. for (const {addedNodes} of mutations) {
  607. for (const node of addedNodes) {
  608. switch (node.tagName) {
  609. case 'YTD-RICH-GRID-ROW':
  610. rows.push(node);
  611. break;
  612. case 'YTD-RICH-SECTION-RENDERER':
  613. sections.push(node);
  614. }
  615. }
  616. }
  617. hideAll(isActive(), rows, sections);
  618. }
  619.  
  620. function resetConfig(fullReset = true) {
  621. hideList.revert(fullReset);
  622. }
  623.  
  624. function getButtonDock() {
  625. return document
  626. .querySelector('ytd-browse[page-subtype="subscriptions"]')
  627. .querySelector('#contents')
  628. .querySelector('#title-container')
  629. .querySelector('#top-level-buttons-computed');
  630. }
  631.  
  632. // Button
  633.  
  634. class ClickHandler {
  635. constructor(button, onShortClick, onLongClick) {
  636. this.onShortClick = function () {
  637. onShortClick();
  638. window.clearTimeout(this.longClickTimeout);
  639. window.removeEventListener('mouseup', this.onShortClick);
  640. }.bind(this);
  641. this.onLongClick = function () {
  642. window.removeEventListener('mouseup', this.onShortClick);
  643. onLongClick();
  644. }.bind(this);
  645. this.longClickTimeout = window.setTimeout(this.onLongClick, LONG_PRESS_TIME);
  646. window.addEventListener('mouseup', this.onShortClick);
  647. }
  648. }
  649.  
  650. class Button {
  651. wasActive;
  652. isActive = false;
  653. isDormant = false;
  654. constructor() {
  655. this.element = (() => {
  656. const getSVG = () => {
  657. const svgNamespace = 'http://www.w3.org/2000/svg';
  658. const bottom = document.createElementNS(svgNamespace, 'path');
  659. 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');
  660. const top = document.createElementNS(svgNamespace, 'rect');
  661. top.setAttribute('x', '13.95');
  662. top.setAttribute('width', '294');
  663. top.setAttribute('height', '45');
  664. const g = document.createElementNS(svgNamespace, 'g');
  665. g.appendChild(bottom);
  666. g.appendChild(top);
  667. const svg = document.createElementNS(svgNamespace, 'svg');
  668. svg.setAttribute('viewBox', '-50 -50 400 400');
  669. svg.setAttribute('focusable', 'false');
  670. svg.appendChild(g);
  671. return svg;
  672. };
  673. const getNewButton = () => {
  674. const {parentElement, 'children': [, openerTemplate]} = getButtonDock();
  675. const button = openerTemplate.cloneNode(false);
  676. if (openerTemplate.innerText) {
  677. throw new Error('too early');
  678. }
  679. parentElement.appendChild(button);
  680. button.innerHTML = openerTemplate.innerHTML;
  681. button.querySelector('yt-button-shape').innerHTML = openerTemplate.querySelector('yt-button-shape').innerHTML;
  682. button.querySelector('a').removeAttribute('href');
  683. button.querySelector('yt-icon').appendChild(getSVG());
  684. button.querySelector('tp-yt-paper-tooltip').remove();
  685. return button;
  686. };
  687. return getNewButton();
  688. })();
  689. this.element.addEventListener('mousedown', this.onMouseDown.bind(this));
  690. GM.getValue(KEY_IS_ACTIVE, true).then((isActive) => {
  691. this.isActive = isActive;
  692. this.update();
  693. const videoObserver = new MutationObserver(hideFromMutations.bind(null, () => this.isActive));
  694. videoObserver.observe(
  695. document.querySelector('ytd-browse[page-subtype="subscriptions"]').querySelector('div#contents'),
  696. {childList: true},
  697. );
  698. hideAll(isActive);
  699. });
  700. let resizeCount = 0;
  701. window.addEventListener('resize', () => {
  702. const resizeId = ++resizeCount;
  703. this.forceInactive();
  704. const listener = ({detail}) => {
  705. // column size changed
  706. if (detail.actionName === 'yt-window-resized') {
  707. window.setTimeout(() => {
  708. if (resizeId !== resizeCount) {
  709. return;
  710. }
  711. this.forceInactive(false);
  712. // Don't bother re-running filters if the sub page isn't shown
  713. if (this.isDormant) {
  714. return;
  715. }
  716. resetConfig();
  717. hideAll(this.isActive);
  718. }, 1000);
  719. document.body.removeEventListener('yt-action', listener);
  720. }
  721. };
  722. document.body.addEventListener('yt-action', listener);
  723. });
  724. }
  725. forceInactive(doForce = true) {
  726. if (doForce) {
  727. // if wasActive isn't undefined, forceInactive was already called
  728. if (this.wasActive === undefined) {
  729. // Saves a GM.getValue call later
  730. this.wasActive = this.isActive;
  731. this.isActive = false;
  732. }
  733. } else {
  734. this.isActive = this.wasActive;
  735. this.wasActive = undefined;
  736. }
  737. }
  738. update() {
  739. if (this.isActive) {
  740. this.setButtonActive();
  741. }
  742. }
  743. setButtonActive() {
  744. if (this.isActive) {
  745. this.element.querySelector('svg').style.setProperty('fill', 'var(--yt-spec-call-to-action)');
  746. } else {
  747. this.element.querySelector('svg').style.setProperty('fill', 'currentcolor');
  748. }
  749. }
  750. toggleActive() {
  751. this.isActive = !this.isActive;
  752. this.setButtonActive();
  753. GM.setValue(KEY_IS_ACTIVE, this.isActive);
  754. if (this.isActive) {
  755. hideList.ensure();
  756. } else {
  757. hideList.revert(false);
  758. }
  759. }
  760. async onLongClick() {
  761. await $config.edit();
  762. resetConfig();
  763. hideAll(this.isActive);
  764. }
  765. onMouseDown(event) {
  766. if (event.button === 0) {
  767. new ClickHandler(this.element, this.toggleActive.bind(this), this.onLongClick.bind(this));
  768. }
  769. }
  770. }
  771.  
  772. // Main
  773.  
  774. (() => {
  775. let button;
  776. const loadButton = async () => {
  777. if (button) {
  778. button.isDormant = false;
  779. hideAll(button.isActive);
  780. return;
  781. }
  782. try {
  783. await $config.ready();
  784. } catch (error) {
  785. if (!$config.reset) {
  786. throw error;
  787. }
  788. if (!window.confirm(`${error.message}\n\nWould you like to erase your data?`)) {
  789. return;
  790. }
  791. $config.reset();
  792. }
  793. try {
  794. getButtonDock();
  795. button = new Button();
  796. } catch (e) {
  797. const emitter = document.getElementById('page-manager');
  798. const bound = () => {
  799. loadButton();
  800. emitter.removeEventListener('yt-action', bound);
  801. };
  802. emitter.addEventListener('yt-action', bound);
  803. }
  804. };
  805. const isGridView = () => {
  806. return Boolean(
  807. document.querySelector('ytd-browse[page-subtype="subscriptions"]:not([hidden])')
  808. && document.querySelector('ytd-browse > ytd-two-column-browse-results-renderer ytd-rich-grid-row ytd-rich-item-renderer ytd-rich-grid-media'),
  809. );
  810. };
  811. function onNavigate({detail}) {
  812. if (detail.endpoint.browseEndpoint) {
  813. const {params, browseId} = detail.endpoint.browseEndpoint;
  814. // Handle navigation to the sub feed
  815. if ((params === 'MAE%3D' || (!params && (!button || isGridView()))) && browseId === 'FEsubscriptions') {
  816. const emitter = document.querySelector('ytd-app');
  817. const event = 'yt-action';
  818. if (button || isGridView()) {
  819. loadButton();
  820. } else {
  821. const listener = ({detail}) => {
  822. if (detail.actionName === 'ytd-update-grid-state-action') {
  823. if (isGridView()) {
  824. loadButton();
  825. }
  826. emitter.removeEventListener(event, listener);
  827. }
  828. };
  829. emitter.addEventListener(event, listener);
  830. }
  831. return;
  832. }
  833. }
  834. // Handle navigation away from the sub feed
  835. if (button) {
  836. button.isDormant = true;
  837. hideList.revert();
  838. }
  839. }
  840. document.body.addEventListener('yt-navigate-finish', onNavigate);
  841. })();