YouTube - Toggle videos buttons

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

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

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