YouTube - Toggle videos buttons

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

当前为 2023-04-27 提交的版本,查看 最新版本

  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.04.27.10.32
  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.  
  23. let buttonsContainer;
  24. let buttonsRow;
  25. let currentUrl;
  26. let toggleUpcomingButton;
  27. let toggleWatchedButton;
  28. let videosTotal;
  29.  
  30. let upcomingHidden = await GM.getValue('upcomingHidden', false);
  31. let watchedHidden = await GM.getValue('watchedHidden', false);
  32.  
  33. const shouldRenderButton = () => {
  34. return location.href.match(urlPattern) !== null;
  35. };
  36.  
  37. const shouldRunScript = () => {
  38. const oldUrl = currentUrl;
  39. currentUrl = location.href.split('?')[0];
  40.  
  41. const oldVideosTotal = videosTotal;
  42. videosTotal = jQuery(videosSelector).length;
  43.  
  44. const locationChanged = !!oldUrl && oldUrl !== currentUrl;
  45. const videosCountChanged = oldVideosTotal !== videosTotal;
  46. const videosShouldBeHidden = (upcomingHidden || watchedHidden) && !!document.querySelectorAll(unprocessedVideosSelectors).length;
  47. const videosShouldBeShown = (!upcomingHidden || !watchedHidden) && !!document.querySelectorAll(processedVideosSelectors).length;
  48.  
  49. const shouldIt = shouldRenderButton() && (locationChanged || videosCountChanged || videosShouldBeHidden || videosShouldBeShown);
  50.  
  51. if (shouldIt) {
  52. debug(`Videos should be processed
  53. locationChanged: ${locationChanged}
  54. videosCountChanged: ${videosCountChanged}
  55. videosShouldBeHidden: ${videosShouldBeHidden}
  56. videosShouldBeShown: ${videosShouldBeShown}`);
  57. }
  58.  
  59. return shouldIt;
  60. };
  61.  
  62. const runButtonTask = () => {
  63. if (shouldRenderButton()) {
  64. const buttonDestinationContainer = jQuery(
  65. buttonDestinationContainerSelector
  66. ).first();
  67.  
  68. if (
  69. buttonDestinationContainer.length &&
  70. !buttonDestinationContainer.find(buttonsContainer).length
  71. ) {
  72. insertButtons(buttonDestinationContainer);
  73. }
  74. } else {
  75. buttonsContainer.remove();
  76. }
  77. };
  78.  
  79. const runVideosTask = () => {
  80. if (shouldRunScript()) {
  81. setTimeout(processAllVideos, 150);
  82. }
  83. };
  84.  
  85. const insertButtons = (buttonDestinationContainer) => {
  86. toggleWatchedButton.off('click').on('click', toggleWatchedVideos);
  87. toggleUpcomingButton.off('click').on('click', toggleUpcomingVideos);
  88. videosTotal = jQuery(videosSelector).length;
  89.  
  90. const params = { matchingVideosCount: 0 };
  91.  
  92. setButtonText(
  93. toggleWatchedButton,
  94. watchedHidden ? i18n.showWatched : i18n.hideWatched,
  95. params
  96. );
  97.  
  98. setButtonText(
  99. toggleUpcomingButton,
  100. upcomingHidden ? i18n.showUpcoming : i18n.hideUpcoming,
  101. params
  102. );
  103.  
  104. buttonDestinationContainer.prepend(buttonsContainer);
  105. };
  106.  
  107. const processAllVideos = () => {
  108. debug(`Processing videos...`);
  109. videosTotal = jQuery(videosSelector).length;
  110. if (upcomingHidden) processUpcomingVideos();
  111. if (watchedHidden) processWatchedVideos();
  112. debug(`All videos processed`);
  113. };
  114.  
  115. const toggleWatchedVideos = () => {
  116. watchedHidden = !watchedHidden;
  117. GM.setValue('watchedHidden', watchedHidden);
  118. processWatchedVideos();
  119. };
  120.  
  121. const toggleUpcomingVideos = () => {
  122. upcomingHidden = !upcomingHidden;
  123. GM.setValue('upcomingHidden', upcomingHidden);
  124. processUpcomingVideos();
  125. };
  126.  
  127. const processWatchedVideos = () => {
  128. processVideos(
  129. watchedHidden,
  130. watchedVideosSelector,
  131. toggleWatchedButton,
  132. watchedHidden ? i18n.showWatched : i18n.hideWatched
  133. );
  134. };
  135.  
  136. const processUpcomingVideos = () => {
  137. processVideos(
  138. upcomingHidden,
  139. upcomingVideosSelector,
  140. toggleUpcomingButton,
  141. upcomingHidden ? i18n.showUpcoming : i18n.hideUpcoming
  142. );
  143. };
  144.  
  145. const processVideos = (hide, matchingSelector, button, text) => {
  146. const matchingVideos = jQuery(matchingSelector).parents(videosSelector);
  147. hide
  148. ? matchingVideos.addClass('mt-hidden')
  149. : matchingVideos.removeClass('mt-hidden');
  150.  
  151. const matchingVideosCount = matchingVideos && matchingVideos.length;
  152. setButtonText(button, text, { matchingVideosCount });
  153. };
  154.  
  155. const setButtonText = (button, text, params) => {
  156. const suffix = params?.matchingVideosCount !== undefined ? `(${params.matchingVideosCount} / ${videosTotal})` : '';
  157. button.text(`${text} ${suffix}`);
  158. };
  159.  
  160. const debug = enableDebug ?
  161. (message) => console.debug(`${scriptPrefix} ${message}`) :
  162. () => {};
  163.  
  164. const initialize = () => {
  165. jQuery('head').append(baseStyle);
  166.  
  167. toggleWatchedButton = jQuery(buttonTemplate);
  168. toggleUpcomingButton = jQuery(buttonTemplate);
  169.  
  170. buttonsRow = jQuery(buttonsRowTemplate);
  171. buttonsRow.append(toggleWatchedButton);
  172. buttonsRow.append(toggleUpcomingButton);
  173.  
  174. buttonsContainer = jQuery(buttonsContainerTemplate);
  175. buttonsContainer.append(buttonsRow);
  176.  
  177. setInterval(runButtonTask, 150);
  178. setInterval(runVideosTask, 1000);
  179.  
  180. console.info(`${scriptPrefix} Script initialized.`);
  181. };
  182.  
  183. const scriptPrefix = `[Toggle videos buttons]`;
  184.  
  185. const urlPattern =
  186. /youtube.com\/((channel\/|c\/|@)(.*)\/videos|feed\/subscriptions|results|playlist)/;
  187.  
  188. // texts
  189. const i18n = {
  190. hide: 'Hide',
  191. show: 'Show',
  192. watched: 'watched',
  193. upcoming: 'upcoming'
  194. };
  195.  
  196. i18n.hideWatched = `${i18n.hide} ${i18n.watched}`;
  197. i18n.hideUpcoming = `${i18n.hide} ${i18n.upcoming}`;
  198.  
  199. i18n.showWatched = `${i18n.show} ${i18n.watched}`;
  200. i18n.showUpcoming = `${i18n.show} ${i18n.upcoming}`;
  201.  
  202. // selectors
  203. const watchedVideosSelector = `[id="progress"]`;
  204.  
  205. const upcomingVideosSelector = `[overlay-style="UPCOMING"]`;
  206.  
  207. const buttonDestinationContainerSelector = `
  208. [page-subtype="channels"][role="main"] ytd-rich-grid-renderer,
  209. [page-subtype="playlist"][role="main"] ytd-item-section-renderer,
  210. [page-subtype="subscriptions"][role="main"] ytd-section-list-renderer,
  211. ytd-search[role="main"] ytd-section-list-renderer
  212. `;
  213.  
  214. const videosSelector = `
  215. [page-subtype="channels"][role="main"] ytd-rich-item-renderer,
  216. [page-subtype="playlist"][role="main"] ytd-playlist-video-renderer,
  217. [page-subtype="subscriptions"][role="main"] ytd-grid-video-renderer,
  218. [page-subtype="subscriptions"][role="main"] ytd-video-renderer,
  219. ytd-search[role="main"] ytd-video-renderer
  220. `;
  221.  
  222. const unprocessedVideosSelectors = videosSelector
  223. .split(',')
  224. .map(
  225. (selector) =>
  226. `${selector}:not(.mt-hidden) ${watchedVideosSelector},\n${selector}:not(.mt-hidden) ${upcomingVideosSelector}`
  227. )
  228. .join(',');
  229.  
  230. const processedVideosSelectors = videosSelector
  231. .split(',')
  232. .map(
  233. (selector) =>
  234. `${selector}:.mt-hidden ${watchedVideosSelector},\n${selector}:.mt-hidden ${upcomingVideosSelector}`
  235. )
  236. .join(',');
  237.  
  238. // templates
  239. const buttonTemplate = `
  240. <tp-yt-paper-button class="style-scope ytd-subscribe-button-renderer mt-toggle-videos-button" />
  241. `;
  242.  
  243. const buttonsContainerTemplate = `
  244. <div class="mt-toggle-videos-container"></div>
  245. `;
  246.  
  247. const buttonsRowTemplate = `
  248. <div class="mt-toggle-videos-buttons-row"></div>
  249. `;
  250.  
  251. // style
  252. const baseStyle = `
  253. <style>
  254. .mt-toggle-videos-container {
  255. width: 100%;
  256. }
  257.  
  258. .mt-toggle-videos-buttons-row {
  259. display: flex;
  260. grid-gap: 8px;
  261. }
  262.  
  263. .mt-toggle-videos-button {
  264. border-radius: 20px !important;
  265. flex: 1;
  266. margin: 0 !important;
  267. }
  268.  
  269. .mt-hidden {
  270. display: none !important;
  271. }
  272.  
  273. [page-subtype="channels"] .mt-toggle-videos-container,
  274. [page-subtype="subscriptions"] .mt-toggle-videos-container {
  275. margin-top: 24px;
  276. }
  277.  
  278. [page-subtype="playlist"] .mt-toggle-videos-container {
  279. box-sizing: border-box;
  280. padding: 0 24px;
  281. }
  282.  
  283. .ytd-search ytd-section-list-renderer .mt-toggle-videos-container {
  284. margin: 12px 0;
  285. }
  286. </style>
  287. `;
  288.  
  289. initialize();
  290. })();