YouTube Grid Row Controller

Adds simple buttons to control items per row on Youtube's home feed, works for shorts and news sections too. Buttons can be hidden if needed.

  1. // ==UserScript==
  2. // @name YouTube Grid Row Controller
  3. // @namespace https://github.com/HageFX-78
  4. // @version 0.5
  5. // @description Adds simple buttons to control items per row on Youtube's home feed, works for shorts and news sections too. Buttons can be hidden if needed.
  6. // @author HageFX78
  7. // @license MIT
  8. // @match *://www.youtube.com/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. // Configurable options
  18. const embedInChips = true; // Only applies to the one that is attached to the categories bar, set false if you have another script that removes the bar
  19. const hideControls = false; // set true to hide UI controls, it will use the default values instead
  20.  
  21. const transparentButtons = false; // set true to make the buttons transparent and less intrusive, only applies if hideControls is false
  22.  
  23. const defaultSettingValue = {
  24. // Default values mainly used when if you want to hide the buttons, change the values to your liking
  25. content: 4,
  26. news: 5,
  27. shorts: 6,
  28. };
  29.  
  30. let currentSettingValues = {
  31. content: GM_getValue('itemPerRow', defaultSettingValue.content),
  32. news: GM_getValue('newsPerRow', defaultSettingValue.news),
  33. shorts: GM_getValue('shortsPerRow', defaultSettingValue.shorts),
  34. };
  35.  
  36. // Styles
  37. const style = (css) => {
  38. const el = document.createElement('style');
  39. el.textContent = css;
  40. document.head.appendChild(el);
  41. return el;
  42. };
  43.  
  44. style(`
  45. ${
  46. hideControls
  47. ? ''
  48. : '#right-arrow {right: 10% !important;} #chips-wrapper {justify-content: left !important;}#chips-content{width: 90% !important;}'
  49. }
  50.  
  51. .justify-left-custom {
  52. justify-content: left !important;
  53. }
  54.  
  55. ytd-rich-item-renderer[rendered-from-rich-grid][is-in-first-column] {
  56. margin-left: calc(var(--ytd-rich-grid-item-margin) / 2) !important;
  57. }
  58. ytd-rich-item-renderer[hidden][is-responsive-grid], [is-slim-media]{
  59. display: block !important;
  60. }
  61.  
  62. ytd-rich-item-renderer{
  63. margin-bottom: var(--ytd-rich-grid-row-margin) !important;
  64. }
  65.  
  66. .button-container.ytd-rich-shelf-renderer {
  67. display: none !important;
  68. }
  69. #dismissible.ytd-rich-shelf-renderer {
  70. padding-bottom: 0 !important;
  71. border-bottom: none !important;
  72. }
  73. .itemPerRowControl {
  74. display: flex;
  75. justify-content: right;
  76. align-items: center;
  77.  
  78. flex: 1;
  79. gap: 10px;
  80. box-sizing: border-box;
  81. user-select: none;
  82. ${embedInChips ? '' : 'width: 100%;'};
  83. }
  84.  
  85. .itemPerRowControl button {
  86.  
  87. border: none;
  88. color: var(--yt-spec-text-primary);
  89. background-color:${transparentButtons ? 'transparent' : 'var(--yt-spec-badge-chip-background)'};
  90. font-size: 24px;
  91. text-align: center;
  92. display: inline-block;
  93.  
  94.  
  95. height: 30px;
  96. aspect-ratio: 1/1;
  97. border-radius: 50%;
  98. }
  99.  
  100. .itemPerRowControl button:hover {
  101. background-color: var(--yt-spec-button-chip-background-hover);
  102. cursor: pointer;
  103. }
  104. `);
  105.  
  106. const dynamicStyle = style('');
  107.  
  108. function updatePageLayout() {
  109. dynamicStyle.textContent = `
  110. ytd-rich-grid-renderer {
  111. --ytd-rich-grid-items-per-row: ${hideControls ? defaultSettingValue.content : currentSettingValues.content} !important;
  112. }
  113. ytd-rich-shelf-renderer:not([is-shorts]) {
  114. --ytd-rich-grid-items-per-row: ${hideControls ? defaultSettingValue.news : currentSettingValues.news} !important;
  115. }
  116. ytd-rich-shelf-renderer[is-shorts] {
  117. --ytd-rich-grid-slim-items-per-row: ${hideControls ? defaultSettingValue.shorts : currentSettingValues.shorts} !important;
  118. --ytd-rich-grid-items-per-row: ${hideControls ? defaultSettingValue.shorts : currentSettingValues.shorts} !important;
  119. }
  120. `;
  121. }
  122.  
  123. function saveValues() {
  124. GM_setValue('itemPerRow', currentSettingValues.content);
  125. GM_setValue('newsPerRow', currentSettingValues.news);
  126. GM_setValue('shortsPerRow', currentSettingValues.shorts);
  127. }
  128.  
  129. function updateAndSave() {
  130. updatePageLayout();
  131. saveValues();
  132. }
  133.  
  134. function waitForElement(baseQuery, selector) {
  135. return new Promise((resolve) => {
  136. const observer = new MutationObserver(() => {
  137. const el = baseQuery.querySelector(selector);
  138. if (el) {
  139. observer.disconnect();
  140. resolve(el);
  141. }
  142. });
  143.  
  144. observer.observe(baseQuery, { childList: true, subtree: true });
  145. });
  146. }
  147.  
  148. function watchMainContent(container) {
  149. const observer = new MutationObserver((mutations) => {
  150. mutations.forEach(({ addedNodes }) => {
  151. for (let node of addedNodes) {
  152. if (node.nodeType === 1 && node.matches('ytd-rich-section-renderer')) {
  153. const ref = node.querySelector('#menu-container');
  154. const hasControlDiv = node.querySelector('.itemPerRowControl') !== null;
  155. const isShorts = node.querySelector('[is-shorts]') !== null;
  156.  
  157. if (hasControlDiv) continue; // Skip if already exists
  158.  
  159. createControlDiv(ref, isShorts ? 'shorts' : 'news', true);
  160. }
  161. }
  162. });
  163. });
  164.  
  165. observer.observe(container, { childList: true, subtree: true });
  166. }
  167.  
  168. function createControlDiv(target, type, insertBefore = false) {
  169. const controlDiv = document.createElement('div');
  170. controlDiv.classList.add('style-scope', 'ytd-rich-grid-renderer', 'itemPerRowControl');
  171.  
  172. ['-', '+'].forEach((symbol) => {
  173. const btn = document.createElement('button');
  174. btn.innerText = symbol;
  175. btn.addEventListener('click', () => {
  176. if (symbol === '+') {
  177. currentSettingValues[type]++;
  178. console.log(currentSettingValues[type]);
  179. } else if (currentSettingValues[type] > 1) {
  180. currentSettingValues[type]--;
  181. }
  182. updateAndSave();
  183. });
  184. controlDiv.appendChild(btn);
  185. });
  186.  
  187. if (insertBefore) target.parentNode.insertBefore(controlDiv, target);
  188. else target.appendChild(controlDiv);
  189. if (!insertBefore) controlDiv.classList.add('justify-left-custom');
  190. }
  191.  
  192. function init(queryStartLocation) {
  193. updatePageLayout();
  194.  
  195. if (hideControls) return;
  196.  
  197. if (embedInChips) {
  198. waitForElement(queryStartLocation, '#chips-wrapper').then((el) => createControlDiv(el, 'content'));
  199. } else {
  200. waitForElement(queryStartLocation, '#contents.ytd-rich-grid-renderer').then((el) => createControlDiv(el, 'content', true));
  201. }
  202.  
  203. // Start watching for newly loaded sections
  204. waitForElement(queryStartLocation, '#contents.ytd-rich-grid-renderer').then(watchMainContent);
  205.  
  206. // Cleanup after init
  207. window.removeEventListener('yt-navigate-finish', handlePageContentChanged);
  208. }
  209.  
  210. // Workaround when reloaded on creator's home page and going back to main page will hide the buttons
  211. let firstLoad = true;
  212. function handlePageContentChanged() {
  213. if (location.href.endsWith('youtube.com/')) {
  214. let browseElements = document.querySelectorAll('ytd-browse');
  215.  
  216. if (firstLoad || browseElements.length <= 1) {
  217. init(browseElements[0]);
  218. } else {
  219. // If reloaded on creator's home page, second ytd-browse will be the main page
  220. init(browseElements[1]);
  221. }
  222. }
  223. firstLoad = false;
  224. }
  225.  
  226. // ----------------------------------- Main Execution -----------------------------------
  227.  
  228. window.addEventListener('yt-navigate-finish', handlePageContentChanged);
  229. })();