YouTube Sub Feed Filter 2

Filters your YouTube subscriptions feed.

当前为 2024-01-09 提交的版本,查看 最新版本

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址