YouTube - Toggle videos buttons

Adds buttons to hide watched and/or upcoming videos from the subscription page / channel videos tab.

当前为 2023-06-13 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube - Toggle videos buttons
  3. // @description Adds buttons to hide watched and/or upcoming videos from the subscription page / channel videos tab.
  4. // @version 2023.06.12.22.59
  5. // @author MetalTxus
  6. // @namespace https://github.com/jesuscc1993
  7.  
  8. // @icon https://www.youtube.com/favicon.ico
  9. // @match *://*.youtube.com/*
  10. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js
  11.  
  12. // @grant GM.getValue
  13. // @grant GM.setValue
  14. // ==/UserScript==
  15.  
  16. /* globals jQuery */
  17.  
  18. (async () => {
  19. 'use strict';
  20.  
  21. const enableDebug = false;
  22. const enableCount = false; // disabled due to corner cases where it would not update
  23.  
  24. let buttonsContainer;
  25. let currentUrl;
  26. let toggleLiveButton;
  27. let toggleShortsButton;
  28. let toggleUpcomingButton;
  29. let toggleUploadsButton;
  30. let toggleWatchedButton;
  31. let toggleButtonsButton;
  32. let videosTotal;
  33.  
  34. let liveHidden = await GM.getValue('liveHidden', false);
  35. let shortsHidden = await GM.getValue('shortsHidden', false);
  36. let upcomingHidden = await GM.getValue('upcomingHidden', false);
  37. let uploadsHidden = await GM.getValue('uploadsHidden', false);
  38. let watchedHidden = await GM.getValue('watchedHidden', false);
  39. let buttonsHidden = await GM.getValue('buttonsHidden', false);
  40.  
  41. const shouldRenderButton = () => {
  42. return location.href.match(urlPattern) !== null;
  43. };
  44.  
  45. const shouldRunScript = () => {
  46. const oldUrl = currentUrl;
  47. currentUrl = location.href.split('?')[0];
  48.  
  49. const oldVideosTotal = videosTotal;
  50. videosTotal = jQuery(videosSelector).length;
  51.  
  52. const locationChanged = !!oldUrl && oldUrl !== currentUrl;
  53. const videosCountChanged = oldVideosTotal !== videosTotal;
  54.  
  55. const videosShouldBeHidden =
  56. (liveHidden ||
  57. shortsHidden ||
  58. upcomingHidden ||
  59. uploadsHidden ||
  60. watchedHidden) &&
  61. !!document.querySelectorAll(unprocessedVideosSelectors).length;
  62.  
  63. const videosShouldBeShown =
  64. !(
  65. liveHidden &&
  66. shortsHidden &&
  67. upcomingHidden &&
  68. uploadsHidden &&
  69. watchedHidden
  70. ) && !!document.querySelectorAll(processedVideosSelectors).length;
  71.  
  72. const shouldIt =
  73. shouldRenderButton() &&
  74. (locationChanged ||
  75. videosCountChanged ||
  76. videosShouldBeHidden ||
  77. videosShouldBeShown);
  78.  
  79. if (shouldIt) {
  80. debug(`Videos should be processed
  81. locationChanged: ${locationChanged}
  82. videosCountChanged: ${videosCountChanged}
  83. videosShouldBeHidden: ${videosShouldBeHidden}
  84. videosShouldBeShown: ${videosShouldBeShown}`);
  85. }
  86.  
  87. return shouldIt;
  88. };
  89.  
  90. const runButtonTask = () => {
  91. if (shouldRenderButton()) {
  92. const buttonDestinationContainer = jQuery(
  93. buttonDestinationContainerSelector
  94. ).first();
  95.  
  96. if (
  97. buttonDestinationContainer.length &&
  98. !buttonDestinationContainer.find(buttonsContainer).length
  99. ) {
  100. insertButtons(buttonDestinationContainer);
  101. }
  102. } else {
  103. buttonsContainer.remove();
  104. toggleButtonsButton.remove();
  105. }
  106. };
  107.  
  108. const runVideosTask = () => {
  109. if (shouldRunScript()) {
  110. setTimeout(processAllVideos, 150);
  111. }
  112. };
  113.  
  114. const insertButtons = (buttonDestinationContainer) => {
  115. toggleLiveButton.off('click').on('click', toggleLiveVideos);
  116. toggleShortsButton.off('click').on('click', toggleShortsVideos);
  117. toggleUpcomingButton.off('click').on('click', toggleUpcomingVideos);
  118. toggleUploadsButton.off('click').on('click', toggleUploadsVideos);
  119. toggleWatchedButton.off('click').on('click', toggleWatchedVideos);
  120. toggleButtonsButton.off('click').on('click', toggleButtons);
  121. videosTotal = jQuery(videosSelector).length;
  122.  
  123. const params = { matchingVideosCount: 0 };
  124. setButtonState(toggleLiveButton, i18n.live, liveHidden, params);
  125. setButtonState(toggleShortsButton, i18n.shorts, shortsHidden, params);
  126. setButtonState(toggleUpcomingButton, i18n.upcoming, upcomingHidden, params);
  127. setButtonState(toggleUploadsButton, i18n.uploads, uploadsHidden, params);
  128. setButtonState(toggleWatchedButton, i18n.watched, watchedHidden, params);
  129.  
  130. buttonDestinationContainer.prepend(buttonsContainer);
  131. jQuery(buttonsToggleDestinationSelector).prepend(toggleButtonsButton);
  132. };
  133.  
  134. const processAllVideos = () => {
  135. debug(`Processing videos...`);
  136. videosTotal = jQuery(videosSelector).length;
  137. if (liveHidden) processLiveVideos();
  138. if (shortsHidden) processShortsVideos();
  139. if (upcomingHidden) processUpcomingVideos();
  140. if (uploadsHidden) processUploadsVideos();
  141. if (watchedHidden) processWatchedVideos();
  142. debug(`All videos processed`);
  143. };
  144.  
  145. const toggleLiveVideos = () => {
  146. liveHidden = !liveHidden;
  147. GM.setValue('liveHidden', liveHidden);
  148. processLiveVideos();
  149. };
  150.  
  151. const toggleShortsVideos = () => {
  152. shortsHidden = !shortsHidden;
  153. GM.setValue('shortsHidden', shortsHidden);
  154. processShortsVideos();
  155. };
  156.  
  157. const toggleUpcomingVideos = () => {
  158. upcomingHidden = !upcomingHidden;
  159. GM.setValue('upcomingHidden', upcomingHidden);
  160. processUpcomingVideos();
  161. };
  162.  
  163. const toggleUploadsVideos = () => {
  164. uploadsHidden = !uploadsHidden;
  165. GM.setValue('uploadsHidden', uploadsHidden);
  166. processUploadsVideos();
  167. };
  168.  
  169. const toggleWatchedVideos = () => {
  170. watchedHidden = !watchedHidden;
  171. GM.setValue('watchedHidden', watchedHidden);
  172. processWatchedVideos();
  173. };
  174.  
  175. const toggleButtons = (newValue) => {
  176. buttonsHidden = typeof newValue == 'boolean' ? newValue : !buttonsHidden;
  177. GM.setValue('buttonsHidden', buttonsHidden);
  178. buttonsHidden
  179. ? buttonsContainer.addClass('hide-buttons')
  180. : buttonsContainer.removeClass('hide-buttons');
  181. };
  182.  
  183. const processLiveVideos = () => {
  184. processVideos(toggleLiveButton, i18n.live, liveHidden, liveVideosSelector);
  185. };
  186.  
  187. const processShortsVideos = () => {
  188. processVideos(
  189. toggleShortsButton,
  190. i18n.shorts,
  191. shortsHidden,
  192. shortsVideosSelector
  193. );
  194. };
  195.  
  196. const processUpcomingVideos = () => {
  197. processVideos(
  198. toggleUpcomingButton,
  199. i18n.upcoming,
  200. upcomingHidden,
  201. upcomingVideosSelector
  202. );
  203. };
  204.  
  205. const processUploadsVideos = () => {
  206. processVideos(
  207. toggleUploadsButton,
  208. i18n.uploads,
  209. uploadsHidden,
  210. uploadsVideosSelector
  211. );
  212. };
  213.  
  214. const processWatchedVideos = () => {
  215. processVideos(
  216. toggleWatchedButton,
  217. i18n.watched,
  218. watchedHidden,
  219. watchedVideosSelector
  220. );
  221. };
  222.  
  223. const processVideos = (button, buttonName, hidden, matchingSelector) => {
  224. const matchingVideos = jQuery(matchingSelector).parents(videosSelector);
  225. hidden
  226. ? matchingVideos.addClass('mt-hidden')
  227. : matchingVideos.removeClass('mt-hidden');
  228. const params = enableCount
  229. ? { matchingVideosCount: matchingVideos && matchingVideos.length }
  230. : undefined;
  231.  
  232. setButtonState(button, buttonName, hidden, params);
  233. };
  234.  
  235. const setButtonState = (button, buttonName, hidden, params) => {
  236. const suffix =
  237. enableCount && params?.matchingVideosCount !== undefined
  238. ? `(${params.matchingVideosCount} / ${videosTotal})`
  239. : '';
  240. button.text(`${buttonName} ${suffix}`);
  241. hidden ? button.removeClass('on') : button.addClass('on');
  242. };
  243.  
  244. const debug = enableDebug
  245. ? (message) => console.debug(`${scriptPrefix} ${message}`)
  246. : () => {};
  247.  
  248. const initialize = () => {
  249. jQuery('head').append(baseStyle);
  250.  
  251. toggleLiveButton = jQuery(toggleVideosButtonTemplate);
  252. toggleShortsButton = jQuery(toggleVideosButtonTemplate);
  253. toggleUpcomingButton = jQuery(toggleVideosButtonTemplate);
  254. toggleUploadsButton = jQuery(toggleVideosButtonTemplate);
  255. toggleWatchedButton = jQuery(toggleVideosButtonTemplate);
  256.  
  257. buttonsContainer = jQuery(buttonsContainerTemplate);
  258. buttonsContainer.append(toggleUpcomingButton);
  259. buttonsContainer.append(toggleLiveButton);
  260. buttonsContainer.append(toggleUploadsButton);
  261. buttonsContainer.append(toggleShortsButton);
  262. buttonsContainer.append(toggleWatchedButton);
  263.  
  264. toggleButtonsButton = jQuery(toggleButtonsButtonTemplate);
  265. toggleButtons(buttonsHidden);
  266.  
  267. setInterval(runButtonTask, 150);
  268. setInterval(runVideosTask, 1000);
  269.  
  270. console.info(`${scriptPrefix} Script initialized.`);
  271. };
  272.  
  273. const scriptPrefix = `[Toggle videos buttons]`;
  274.  
  275. const urlPattern =
  276. /youtube.com\/((channel\/|c\/|@)(.*)\/videos|feed\/subscriptions|results|playlist)/;
  277.  
  278. // texts
  279. const i18n = {
  280. hide: 'Hide',
  281. show: 'Show',
  282. toggleButtons: 'Toggle video filter buttons',
  283.  
  284. live: 'live',
  285. shorts: 'shorts',
  286. upcoming: 'upcoming',
  287. uploads: 'videos',
  288. watched: 'watched',
  289. };
  290.  
  291. // selectors
  292. const liveVideosSelector = `.badge-style-type-live-now-alternate`;
  293. const shortsVideosSelector = `ytd-thumbnail-overlay-time-status-renderer[overlay-style="SHORTS"]`;
  294. const upcomingVideosSelector = `ytd-thumbnail-overlay-time-status-renderer[overlay-style="UPCOMING"]`;
  295. const uploadsVideosSelector = `ytd-thumbnail-overlay-time-status-renderer:not([overlay-style="SHORTS"])`;
  296. const watchedVideosSelector = `[id="progress"]`;
  297.  
  298. const buttonDestinationContainerSelector = `
  299. [page-subtype="channels"][role="main"] ytd-rich-grid-renderer,
  300. [page-subtype="playlist"][role="main"] ytd-item-section-renderer,
  301. [page-subtype="subscriptions"][role="main"] ytd-shelf-renderer,
  302. ytd-search[role="main"] ytd-section-list-renderer
  303. `;
  304.  
  305. const buttonsToggleDestinationSelector = `#masthead #end`;
  306.  
  307. const videosSelector = `
  308. [page-subtype="channels"][role="main"] ytd-rich-item-renderer,
  309. [page-subtype="playlist"][role="main"] ytd-playlist-video-renderer,
  310. [page-subtype="subscriptions"][role="main"] ytd-rich-item-renderer,
  311. ytd-search[role="main"] ytd-video-renderer
  312. `;
  313.  
  314. const unprocessedVideosSelectors = videosSelector
  315. .replace(/\n\s*/g, '')
  316. .split(',')
  317. .map(
  318. (selector) =>
  319. `${selector}:not(.mt-hidden) ${watchedVideosSelector}, ${selector}:not(.mt-hidden) ${upcomingVideosSelector}`
  320. )
  321. .join(',');
  322.  
  323. const processedVideosSelectors = videosSelector
  324. .replace(/\n\s*/g, '')
  325. .split(',')
  326. .map(
  327. (selector) =>
  328. `${selector}.mt-hidden ${watchedVideosSelector}, ${selector}.mt-hidden ${upcomingVideosSelector}`
  329. )
  330. .join(',');
  331.  
  332. // templates
  333. const toggleVideosButtonTemplate = `
  334. <tp-yt-paper-button class="ytd-subscribe-button-renderer mt-button mt-toggle-videos-button" />
  335. `;
  336.  
  337. const toggleButtonsButtonTemplate = `
  338. <tp-yt-paper-button class="mt-button mt-toggle-buttons-button">
  339. <svg viewBox="0 0 24 24">
  340. <g>
  341. <path fill="#FFF" d="M20,7H4V6h16V7z M22,9v12H2V9H22z M15,15l-5-3v6L15,15z M17,3H7v1h10V3z"></path>
  342. </g>
  343. </svg>
  344. <tp-yt-paper-tooltip class="ytd-topbar-menu-button-renderer">
  345. ${i18n.toggleButtons}
  346. </tp-yt-paper-tooltip>
  347. </tp-yt-paper-button>
  348. `;
  349.  
  350. const buttonsContainerTemplate = `
  351. <div class="mt-toggle-videos-container"></div>
  352. `;
  353.  
  354. // style
  355. const baseStyle = `
  356. <style>
  357. .mt-toggle-videos-container {
  358. display: flex;
  359. justify-content: right;
  360. width: 374px; /* 612px if count enabled */
  361. margin: 0 auto;
  362. }
  363.  
  364. .mt-toggle-videos-container.hide-buttons {
  365. display: none;
  366. }
  367.  
  368. .mt-button {
  369. border-radius: 20px !important;
  370. }
  371.  
  372. .mt-toggle-videos-button {
  373. border-radius: 0 !important;
  374. margin: 0 !important;
  375. text-align: center;
  376. min-width: 112px; /* 192px if count enabled */
  377. background: var(--yt-spec-additive-background) !important;
  378. }
  379. .mt-toggle-videos-button.on {
  380. background: var(--yt-spec-10-percent-layer) !important;
  381. }
  382. .mt-toggle-videos-button:first-child {
  383. border-radius: 20px 0 0 20px !important;
  384. }
  385. .mt-toggle-videos-button:last-child {
  386. border-radius: 0 20px 20px 0 !important;
  387. }
  388.  
  389. .mt-toggle-buttons-button {
  390. background: transparent !important;
  391. height: 40px;
  392. margin: 0 8px 0 0;
  393. min-width: 40px;
  394. padding: 0 !important;
  395. }
  396. .mt-toggle-buttons-button:hover {
  397. background: var(--yt-spec-10-percent-layer) !important;
  398. }
  399. .mt-toggle-buttons-button svg {
  400. width: 24px;
  401. }
  402.  
  403. .mt-hidden {
  404. display: none !important;
  405. }
  406.  
  407. [page-subtype="channels"] .mt-toggle-videos-container {
  408. margin-top: 24px;
  409. }
  410.  
  411. [page-subtype="playlist"] .mt-toggle-videos-container {
  412. box-sizing: border-box;
  413. padding: 0 24px;
  414. }
  415.  
  416. .ytd-search ytd-section-list-renderer .mt-toggle-videos-container {
  417. margin: 12px 0;
  418. }
  419. </style>
  420. `;
  421.  
  422. initialize();
  423. })();