YouTube Smaller Thumbnails

Adds 3 additional thumbnails per row

目前为 2025-04-22 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Smaller Thumbnails
  3. // @namespace http://greasyfork.org
  4. // @version 0.0.3
  5. // @description Adds 3 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 none
  12. // ==/UserScript==
  13. (function() {
  14. 'use strict';
  15. const DELTA = 3; // Amount of columns to add.
  16. const MAX_COLUMNS = 6; // Maximum amount of columns.
  17.  
  18. const DELTA_SHORTS = 9; // Amount of columns to add.
  19. const MAX_SHORTS_COLUMNS = 12; // Maximum amount of columns for shorts.
  20.  
  21. document.addEventListener("DOMContentLoaded", function() {
  22. var style = document.createElement('style');
  23. style.innerHTML = `
  24. ytd-rich-item-renderer[is-slim-media] {
  25. width: 10%;
  26. }
  27. `;
  28. document.body.appendChild(style);
  29. });
  30.  
  31. function installStyle(contents) {
  32. var style = document.createElement('style');
  33. style.innerHTML = contents;
  34. document.body.appendChild(style);
  35. }
  36.  
  37. function getTargetValue(currentValue) {
  38. let newValue = currentValue + DELTA;
  39.  
  40. if (newValue > MAX_COLUMNS) {
  41. newValue = MAX_COLUMNS;
  42. }
  43.  
  44. return newValue;
  45. }
  46.  
  47. function getShortsTargetValue(currentValue) {
  48. let newValue = currentValue + DELTA_SHORTS;
  49.  
  50. if (newValue > MAX_SHORTS_COLUMNS) {
  51. newValue = MAX_SHORTS_COLUMNS;
  52. }
  53.  
  54. return newValue;
  55. }
  56.  
  57. function isShorts(itemElement) {
  58. return null !== itemElement.getAttribute('is-slim-media')
  59. }
  60.  
  61. function modifyGridStyle(gridElement) {
  62. const currentStyle = gridElement.getAttribute('style');
  63. if (!currentStyle) {
  64. return;
  65. }
  66.  
  67. const itemsPerRowMatch = currentStyle.match(/--ytd-rich-grid-items-per-row:\s*(\d+)/);
  68. if (!itemsPerRowMatch) {
  69. return;
  70. }
  71.  
  72. const currentValue = parseInt(itemsPerRowMatch[1], 10);
  73.  
  74. if (isNaN(currentValue)) {
  75. return;
  76. }
  77.  
  78. const newValue = getTargetValue(currentValue);
  79.  
  80. if (currentValue === newValue) {
  81. return;
  82. }
  83.  
  84. const newStyle = currentStyle.replace(
  85. /--ytd-rich-grid-items-per-row:\s*\d+/,
  86. `--ytd-rich-grid-items-per-row: ${newValue}`
  87. );
  88.  
  89. gridElement.setAttribute('style', newStyle);
  90. console.log(`[YouTube Smaller Thumbnails] Modified items per row: ${currentValue} -> ${newValue}`);
  91. }
  92.  
  93. function modifyItemsPerRow(itemElement) {
  94. const currentValue = parseInt(itemElement.getAttribute('items-per-row'), 10);
  95.  
  96. if (isNaN(currentValue)) {
  97. return;
  98. }
  99.  
  100. const newValue = isShorts(itemElement) ?
  101. getShortsTargetValue(currentValue) :
  102. getTargetValue(currentValue);
  103.  
  104. if (currentValue === newValue) {
  105. return;
  106. }
  107.  
  108. itemElement.setAttribute('items-per-row', newValue);
  109. console.log(`[YouTube Smaller Thumbnails] Modified items per row: ${currentValue} -> ${newValue}`);
  110. }
  111.  
  112. function modifyShortHidden(itemElement) {
  113. if (!isShorts(itemElement)) {
  114. return;
  115. }
  116.  
  117. if (null === itemElement.getAttribute('hidden')) {
  118. return
  119. }
  120.  
  121. itemElement.removeAttribute('hidden');
  122. console.log(`[YouTube Smaller Thumbnails] Modified hidden`);
  123. }
  124.  
  125. function modifyShelfRenderer(itemElement) {
  126. const currentStyle = itemElement.getAttribute('style');
  127. if (!currentStyle) {
  128. return;
  129. }
  130.  
  131. const itemsCountMatch = currentStyle.match(/--ytd-rich-shelf-items-count:\s*(\d+)/);
  132. if (!itemsCountMatch) {
  133. return;
  134. }
  135.  
  136. const currentValue = parseInt(itemElement.getAttribute('elements-per-row'), 10);
  137. if (isNaN(currentValue)) {
  138. return;
  139. }
  140.  
  141. const newValue = getShortsTargetValue(currentValue)
  142. if (currentValue === newValue) {
  143. return;
  144. }
  145.  
  146. const newStyle = currentStyle.replace(
  147. /--ytd-rich-shelf-items-count:\s*\d+/,
  148. `--ytd-rich-shelf-items-count: ${newValue}`
  149. );
  150.  
  151. itemElement.setAttribute('style', newStyle);
  152. itemElement.setAttribute('elements-per-row', newValue);
  153. console.log(`[YouTube Smaller Thumbnails] Modified elements per row: ${currentValue} -> ${newValue}`);
  154. }
  155.  
  156. function processExistingElements() {
  157. document.querySelectorAll('ytd-rich-grid-renderer').forEach(gridElement => {
  158. modifyGridStyle(gridElement);
  159. });
  160.  
  161. document.querySelectorAll('ytd-rich-item-renderer').forEach(itemElement => {
  162. modifyItemsPerRow(itemElement);
  163. modifyShortHidden(itemElement);
  164. });
  165. }
  166.  
  167. const observer = new MutationObserver((mutations) => {
  168. mutations.forEach((mutation) => {
  169. if (mutation.addedNodes && mutation.addedNodes.length > 0) {
  170. mutation.addedNodes.forEach((node) => {
  171. if (node.nodeType === Node.ELEMENT_NODE) {
  172. if (node.tagName === 'YTD-RICH-GRID-RENDERER') {
  173. modifyGridStyle(node);
  174. }
  175. if (node.tagName === 'YTD-RICH-ITEM-RENDERER') {
  176. modifyItemsPerRow(node);
  177. }
  178. if (node.tagName === 'YTD-RICH-SHELF-RENDERER') {
  179. modifyShelfRenderer(node);
  180. }
  181.  
  182. node.querySelectorAll('ytd-rich-grid-renderer').forEach(gridElement => {
  183. modifyGridStyle(gridElement);
  184. });
  185. node.querySelectorAll('ytd-rich-item-renderer').forEach(itemElement => {
  186. modifyItemsPerRow(itemElement);
  187. modifyShortHidden(itemElement);
  188. });
  189. node.querySelectorAll('ytd-rich-shelf-renderer').forEach(itemElement => {
  190. modifyShelfRenderer(itemElement);
  191. });
  192. }
  193. });
  194. }
  195.  
  196. if (mutation.type === 'attributes') {
  197. const target = mutation.target;
  198.  
  199. if (target.tagName === 'YTD-RICH-GRID-RENDERER' && mutation.attributeName === 'style') {
  200. modifyGridStyle(target);
  201. }
  202. if (target.tagName === 'YTD-RICH-ITEM-RENDERER' && mutation.attributeName === 'items-per-row') {
  203. if (mutation.attributeName === 'items-per-row') {
  204. modifyItemsPerRow(target);
  205. }
  206.  
  207. if (mutation.attributeName === 'hidden') {
  208. modifyShortHidden(target);
  209. }
  210.  
  211. }
  212. if (target.tagName === 'YTD-RICH-SHELF-RENDERER' && mutation.attributeName === 'elements-per-row') {
  213. modifyShelfRenderer(target);
  214. }
  215. }
  216. });
  217. });
  218.  
  219. function startObserver() {
  220. processExistingElements();
  221. observer.observe(document.documentElement, {
  222. childList: true,
  223. subtree: true,
  224. attributes: true,
  225. attributeFilter: ['style', 'hidden', 'items-per-row', 'elements-per-row']
  226. });
  227.  
  228. console.log('[YouTube Smaller Thumbnails] Observer started');
  229. }
  230.  
  231. if (document.readyState === 'loading') {
  232. document.addEventListener('DOMContentLoaded', startObserver);
  233. } else {
  234. startObserver();
  235. }
  236.  
  237. setInterval(processExistingElements, 3000);
  238. })();