YouTube Smaller Thumbnails

Adds additional thumbnails per row

  1. // ==UserScript==
  2. // @name YouTube Smaller Thumbnails
  3. // @namespace http://greatest.deepsurf.us
  4. // @version 0.0.5
  5. // @description Adds additional thumbnails per row
  6. // @author you
  7. // @license MIT
  8. // @match *://www.youtube.com/*
  9. // @match *://youtube.com/*
  10. // @run-at document-start
  11. // @grant GM_registerMenuCommand
  12. // @grant GM_unregisterMenuCommand
  13. // @grant GM_addValueChangeListener
  14. // @grant GM_getValue
  15. // @grant GM_setValue
  16. // @grant GM_deleteValue
  17. // @require https://update.greatest.deepsurf.us/scripts/470224/1506547/Tampermonkey%20Config.js
  18. // ==/UserScript==
  19. (function() {
  20. 'use strict';
  21. const DEFAULT_MAX_COLUMNS = 6; // Maximum amount of columns.
  22. const DEFAULT_MAX_SHORTS_COLUMNS = 12; // Maximum amount of columns for shorts.
  23.  
  24. const cfg = new GM_config({
  25. columns: {
  26. type: 'int',
  27. name: 'Videos Per Row',
  28. value: DEFAULT_MAX_COLUMNS,
  29. min: 1,
  30. max: 20
  31. },
  32. shortsColumns: {
  33. type: 'int',
  34. name: 'Shorts Per Row',
  35. value: DEFAULT_MAX_SHORTS_COLUMNS,
  36. min: 1,
  37. max: 20
  38. },
  39. shortsScale: {
  40. type: 'int',
  41. name: 'Shorts Scale (in %)',
  42. min: 10,
  43. max: 200,
  44. value: 10
  45. },
  46. applyStyles: {
  47. type: 'boolean',
  48. name: 'Apply Styles',
  49. value: true
  50. }
  51. })
  52.  
  53. function debug(...args) {
  54. console.log('%c[YouTube Smaller Thumbnails]', 'background: #111; color: green; font-weight: bold;', ...args)
  55. }
  56.  
  57. function applyStyles() {
  58. if (!cfg.get('applyStyles')) {
  59. return
  60. }
  61.  
  62. var style = document.createElement('style');
  63. style.appendChild(document.createTextNode(`
  64. ytd-rich-item-renderer[is-slim-media] {
  65. width: ${cfg.get('shortsScale')}% !important;
  66. }
  67. `));
  68. document.body.appendChild(style);
  69. debug('Applied styles')
  70. }
  71.  
  72. document.addEventListener("DOMContentLoaded", applyStyles);
  73. document.addEventListener("load", applyStyles);
  74.  
  75.  
  76. function installStyle(contents) {
  77. var style = document.createElement('style');
  78. style.innerHTML = contents;
  79. document.body.appendChild(style);
  80. }
  81.  
  82. function getTargetValue() {
  83. return currentOrDefault(+cfg.get('columns'), DEFAULT_MAX_COLUMNS)
  84. }
  85.  
  86. function getShortsTargetValue() {
  87. return currentOrDefault(+cfg.get('shortsColumns'), DEFAULT_MAX_SHORTS_COLUMNS)
  88. }
  89.  
  90. function currentOrDefault(value, defaultValue) {
  91. const num = parseInt(value, 10);
  92. if (!isNaN(num) && num.toString() === String(value).trim() && num > 0 && num < 100) {
  93. return num
  94. }
  95. return defaultValue
  96. }
  97.  
  98. function isShorts(itemElement) {
  99. return null !== itemElement.getAttribute('is-slim-media')
  100. }
  101.  
  102. function modifyGridStyle(gridElement) {
  103. const currentStyle = gridElement.getAttribute('style');
  104. if (!currentStyle) {
  105. return;
  106. }
  107.  
  108. const itemsPerRowMatch = currentStyle.match(/--ytd-rich-grid-items-per-row:\s*(\d+)/);
  109. if (!itemsPerRowMatch) {
  110. return;
  111. }
  112.  
  113. const currentValue = parseInt(itemsPerRowMatch[1], 10);
  114.  
  115. if (isNaN(currentValue)) {
  116. return;
  117. }
  118.  
  119. const newValue = getTargetValue();
  120.  
  121. if (currentValue === newValue) {
  122. return;
  123. }
  124.  
  125. const newStyle = currentStyle.replace(
  126. /--ytd-rich-grid-items-per-row:\s*\d+/,
  127. `--ytd-rich-grid-items-per-row: ${newValue}`
  128. );
  129.  
  130. gridElement.setAttribute('style', newStyle);
  131. debug(`Modified items per row: ${currentValue} -> ${newValue}`);
  132. }
  133.  
  134. function modifyItemsPerRow(itemElement) {
  135. const currentValue = parseInt(itemElement.getAttribute('items-per-row'), 10);
  136.  
  137. if (isNaN(currentValue)) {
  138. return;
  139. }
  140.  
  141. const newValue = isShorts(itemElement) ?
  142. getShortsTargetValue() :
  143. getTargetValue();
  144.  
  145. if (currentValue === newValue) {
  146. return;
  147. }
  148.  
  149. itemElement.setAttribute('items-per-row', newValue);
  150. debug(`Modified items per row: ${currentValue} -> ${newValue}`);
  151. }
  152.  
  153. function modifyShortHidden(itemElement) {
  154. if (!isShorts(itemElement)) {
  155. return;
  156. }
  157.  
  158. if (null === itemElement.getAttribute('hidden')) {
  159. return
  160. }
  161.  
  162. itemElement.removeAttribute('hidden');
  163. debug(`Modified hidden`);
  164. }
  165.  
  166. function modifyShelfRenderer(itemElement) {
  167. const currentStyle = itemElement.getAttribute('style');
  168. if (!currentStyle) {
  169. return;
  170. }
  171.  
  172. const itemsCountMatch = currentStyle.match(/--ytd-rich-shelf-items-count:\s*(\d+)/);
  173. if (!itemsCountMatch) {
  174. return;
  175. }
  176.  
  177. const currentValue = parseInt(itemElement.getAttribute('elements-per-row'), 10);
  178. if (isNaN(currentValue)) {
  179. return;
  180. }
  181.  
  182. const newValue = getShortsTargetValue()
  183. if (currentValue === newValue) {
  184. return;
  185. }
  186.  
  187. const newStyle = currentStyle.replace(
  188. /--ytd-rich-shelf-items-count:\s*\d+/,
  189. `--ytd-rich-shelf-items-count: ${newValue}`
  190. );
  191.  
  192. itemElement.setAttribute('style', newStyle);
  193. itemElement.setAttribute('elements-per-row', newValue);
  194. debug(`Modified elements per row: ${currentValue} -> ${newValue}`);
  195. }
  196.  
  197. function processExistingElements() {
  198. document.querySelectorAll('ytd-rich-grid-renderer').forEach(gridElement => {
  199. modifyGridStyle(gridElement);
  200. });
  201.  
  202. document.querySelectorAll('ytd-rich-item-renderer').forEach(itemElement => {
  203. modifyItemsPerRow(itemElement);
  204. modifyShortHidden(itemElement);
  205. });
  206. }
  207.  
  208. const observer = new MutationObserver((mutations) => {
  209. mutations.forEach((mutation) => {
  210. if (mutation.addedNodes && mutation.addedNodes.length > 0) {
  211. mutation.addedNodes.forEach((node) => {
  212. if (node.nodeType === Node.ELEMENT_NODE) {
  213. if (node.tagName === 'YTD-RICH-GRID-RENDERER') {
  214. modifyGridStyle(node);
  215. }
  216. if (node.tagName === 'YTD-RICH-ITEM-RENDERER') {
  217. modifyItemsPerRow(node);
  218. }
  219. if (node.tagName === 'YTD-RICH-SHELF-RENDERER') {
  220. modifyShelfRenderer(node);
  221. }
  222.  
  223. node.querySelectorAll('ytd-rich-grid-renderer').forEach(gridElement => {
  224. modifyGridStyle(gridElement);
  225. });
  226. node.querySelectorAll('ytd-rich-item-renderer').forEach(itemElement => {
  227. modifyItemsPerRow(itemElement);
  228. modifyShortHidden(itemElement);
  229. });
  230. node.querySelectorAll('ytd-rich-shelf-renderer').forEach(itemElement => {
  231. modifyShelfRenderer(itemElement);
  232. });
  233. }
  234. });
  235. }
  236.  
  237. if (mutation.type === 'attributes') {
  238. const target = mutation.target;
  239.  
  240. if (target.tagName === 'YTD-RICH-GRID-RENDERER' && mutation.attributeName === 'style') {
  241. modifyGridStyle(target);
  242. }
  243. if (target.tagName === 'YTD-RICH-ITEM-RENDERER' && mutation.attributeName === 'items-per-row') {
  244. if (mutation.attributeName === 'items-per-row') {
  245. modifyItemsPerRow(target);
  246. }
  247.  
  248. if (mutation.attributeName === 'hidden') {
  249. modifyShortHidden(target);
  250. }
  251.  
  252. }
  253. if (target.tagName === 'YTD-RICH-SHELF-RENDERER' && mutation.attributeName === 'elements-per-row') {
  254. modifyShelfRenderer(target);
  255. }
  256. }
  257. });
  258. });
  259.  
  260. function startObserver() {
  261. processExistingElements();
  262. observer.observe(document.documentElement, {
  263. childList: true,
  264. subtree: true,
  265. attributes: true,
  266. attributeFilter: ['style', 'hidden', 'items-per-row', 'elements-per-row']
  267. });
  268.  
  269. debug('Observer started');
  270. }
  271.  
  272. if (document.readyState === 'loading') {
  273. document.addEventListener('DOMContentLoaded', startObserver);
  274. } else {
  275. startObserver();
  276. }
  277.  
  278. setInterval(processExistingElements, 3000);
  279. })();