YouTube Smaller Thumbnails

Adds additional thumbnails per row

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