YouTube Smaller Thumbnails

Adds additional thumbnails per row

  1. // ==UserScript==
  2. // @name YouTube Smaller Thumbnails
  3. // @namespace http://greasyfork.org
  4. // @version 0.0.7
  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. ytd-rich-item-renderer[rendered-from-rich-grid][is-in-first-column] {
  92. margin-left: calc(var(--ytd-rich-grid-item-margin)/2) !important;
  93. }
  94. `));
  95. document.body.appendChild(style);
  96. debug('Applied styles')
  97. }
  98.  
  99. document.addEventListener("DOMContentLoaded", applyStyles);
  100. document.addEventListener("load", applyStyles);
  101.  
  102.  
  103. function installStyle(contents) {
  104. var style = document.createElement('style');
  105. style.innerHTML = contents;
  106. document.body.appendChild(style);
  107. }
  108.  
  109. function getTargetValue() {
  110. return currentOrDefault(+cfg.get('columns'), DEFAULT_MAX_COLUMNS)
  111. }
  112.  
  113. function getShortsTargetValue() {
  114. return currentOrDefault(+cfg.get('shortsColumns'), DEFAULT_MAX_SHORTS_COLUMNS)
  115. }
  116.  
  117. function currentOrDefault(value, defaultValue) {
  118. const num = parseInt(value, 10);
  119. if (!isNaN(num) && num.toString() === String(value).trim() && num > 0 && num < 100) {
  120. return num
  121. }
  122. return defaultValue
  123. }
  124.  
  125. function isShorts(itemElement) {
  126. return null !== itemElement.getAttribute('is-slim-media')
  127. }
  128.  
  129. function modifyGridStyle(gridElement) {
  130. const currentStyle = gridElement.getAttribute('style');
  131. if (!currentStyle) {
  132. return;
  133. }
  134.  
  135. const itemsPerRowMatch = currentStyle.match(/--ytd-rich-grid-items-per-row:\s*(\d+)/);
  136. if (!itemsPerRowMatch) {
  137. return;
  138. }
  139.  
  140. const currentValue = parseInt(itemsPerRowMatch[1], 10);
  141.  
  142. if (isNaN(currentValue)) {
  143. return;
  144. }
  145.  
  146. const newValue = getTargetValue();
  147.  
  148. if (currentValue === newValue) {
  149. return;
  150. }
  151.  
  152. const newStyle = currentStyle.replace(
  153. /--ytd-rich-grid-items-per-row:\s*\d+/,
  154. `--ytd-rich-grid-items-per-row: ${newValue}`
  155. );
  156.  
  157. gridElement.setAttribute('style', newStyle);
  158. debug(`Modified items per row: ${currentValue} -> ${newValue}`);
  159. }
  160.  
  161. function modifyItemsPerRow(itemElement) {
  162. const currentValue = parseInt(itemElement.getAttribute('items-per-row'), 10);
  163.  
  164. if (isNaN(currentValue)) {
  165. return;
  166. }
  167.  
  168. const newValue = isShorts(itemElement) ?
  169. getShortsTargetValue() :
  170. getTargetValue();
  171.  
  172. if (currentValue === newValue) {
  173. return;
  174. }
  175.  
  176. itemElement.setAttribute('items-per-row', newValue);
  177. debug(`Modified items per row: ${currentValue} -> ${newValue}`);
  178. }
  179.  
  180. function modifyShortHidden(itemElement) {
  181. if (!isShorts(itemElement)) {
  182. return;
  183. }
  184.  
  185. if (null === itemElement.getAttribute('hidden')) {
  186. return
  187. }
  188.  
  189. itemElement.removeAttribute('hidden');
  190. debug(`Modified hidden`);
  191. }
  192.  
  193. function modifyShelfRenderer(itemElement) {
  194. const currentStyle = itemElement.getAttribute('style');
  195. if (!currentStyle) {
  196. return;
  197. }
  198.  
  199. const itemsCountMatch = currentStyle.match(/--ytd-rich-shelf-items-count:\s*(\d+)/);
  200. if (!itemsCountMatch) {
  201. return;
  202. }
  203.  
  204. const currentValue = parseInt(itemElement.getAttribute('elements-per-row'), 10);
  205. if (isNaN(currentValue)) {
  206. return;
  207. }
  208.  
  209. const newValue = getShortsTargetValue()
  210. if (currentValue === newValue) {
  211. return;
  212. }
  213.  
  214. const newStyle = currentStyle.replace(
  215. /--ytd-rich-shelf-items-count:\s*\d+/,
  216. `--ytd-rich-shelf-items-count: ${newValue}`
  217. );
  218.  
  219. itemElement.setAttribute('style', newStyle);
  220. itemElement.setAttribute('elements-per-row', newValue);
  221. debug(`Modified elements per row: ${currentValue} -> ${newValue}`);
  222. }
  223.  
  224. function processExistingElements() {
  225. document.querySelectorAll('ytd-rich-grid-renderer').forEach(gridElement => {
  226. modifyGridStyle(gridElement);
  227. });
  228.  
  229. document.querySelectorAll('ytd-rich-item-renderer').forEach(itemElement => {
  230. modifyItemsPerRow(itemElement);
  231. modifyShortHidden(itemElement);
  232. });
  233. }
  234.  
  235. const observer = new MutationObserver((mutations) => {
  236. mutations.forEach((mutation) => {
  237. if (mutation.addedNodes && mutation.addedNodes.length > 0) {
  238. mutation.addedNodes.forEach((node) => {
  239. if (node.nodeType === Node.ELEMENT_NODE) {
  240. if (node.tagName === 'YTD-RICH-GRID-RENDERER') {
  241. modifyGridStyle(node);
  242. }
  243. if (node.tagName === 'YTD-RICH-ITEM-RENDERER') {
  244. modifyItemsPerRow(node);
  245. }
  246. if (node.tagName === 'YTD-RICH-SHELF-RENDERER') {
  247. modifyShelfRenderer(node);
  248. }
  249.  
  250. node.querySelectorAll('ytd-rich-grid-renderer').forEach(gridElement => {
  251. modifyGridStyle(gridElement);
  252. });
  253. node.querySelectorAll('ytd-rich-item-renderer').forEach(itemElement => {
  254. modifyItemsPerRow(itemElement);
  255. modifyShortHidden(itemElement);
  256. });
  257. node.querySelectorAll('ytd-rich-shelf-renderer').forEach(itemElement => {
  258. modifyShelfRenderer(itemElement);
  259. });
  260. }
  261. });
  262. }
  263.  
  264. if (mutation.type === 'attributes') {
  265. const target = mutation.target;
  266.  
  267. if (target.tagName === 'YTD-RICH-GRID-RENDERER' && mutation.attributeName === 'style') {
  268. modifyGridStyle(target);
  269. }
  270. if (target.tagName === 'YTD-RICH-ITEM-RENDERER' && mutation.attributeName === 'items-per-row') {
  271. if (mutation.attributeName === 'items-per-row') {
  272. modifyItemsPerRow(target);
  273. }
  274.  
  275. if (mutation.attributeName === 'hidden') {
  276. modifyShortHidden(target);
  277. }
  278.  
  279. }
  280. if (target.tagName === 'YTD-RICH-SHELF-RENDERER' && mutation.attributeName === 'elements-per-row') {
  281. modifyShelfRenderer(target);
  282. }
  283. }
  284. });
  285. });
  286.  
  287. function startObserver() {
  288. processExistingElements();
  289. observer.observe(document.documentElement, {
  290. childList: true,
  291. subtree: true,
  292. attributes: true,
  293. attributeFilter: ['style', 'hidden', 'items-per-row', 'elements-per-row']
  294. });
  295.  
  296. debug('Observer started');
  297. }
  298.  
  299. if (document.readyState === 'loading') {
  300. document.addEventListener('DOMContentLoaded', startObserver);
  301. } else {
  302. startObserver();
  303. }
  304.  
  305. setInterval(processExistingElements, 3000);
  306. })();