YouTube - Toggle videos buttons

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

当前为 2023-05-21 提交的版本,查看 最新版本

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