YouTube - Toggle videos buttons

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

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

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