YouTube Layout Master

每行合并 6/5 个缩略图 (自适应)、删除 Shorts、禁用 AV1/WebRTC、添加视频适配切换、清理 URL,并为小屏幕优化字体大小。

  1. // ==UserScript==
  2. // @name YouTube Layout Master
  3. // @namespace eeeeee
  4. // @version 0.4
  5. // @description 每行合并 6/5 个缩略图 (自适应)、删除 Shorts、禁用 AV1/WebRTC、添加视频适配切换、清理 URL,并为小屏幕优化字体大小。
  6. // @author justin
  7. // @match https://*.youtube.com/*
  8. // @exclude https://accounts.youtube.com/*
  9. // @exclude https://studio.youtube.com/*
  10. // @exclude https://music.youtube.com/*
  11. // @grant GM_addStyle
  12. // @grant unsafeWindow
  13. // @run-at document-start
  14. // @license MIT; https://opensource.org/licenses/MIT
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. // --- Helper Functions from Bilibili Script ---
  21. const o$1 = () => {}; // No-op function
  22. const noopNeverResolvedPromise = () => new Promise(o$1);
  23.  
  24. /* eslint-disable no-restricted-globals -- logger */
  25. const consoleLog = unsafeWindow.console.log;
  26. const consoleError = unsafeWindow.console.error;
  27. const consoleWarn = unsafeWindow.console.warn;
  28. const consoleInfo = unsafeWindow.console.info;
  29. const consoleDebug = unsafeWindow.console.debug;
  30. const consoleTrace = unsafeWindow.console.trace;
  31. const consoleGroup = unsafeWindow.console.group;
  32. const consoleGroupCollapsed = unsafeWindow.console.groupCollapsed;
  33. const consoleGroupEnd = unsafeWindow.console.groupEnd;
  34. const logger = {
  35. log: consoleLog.bind(console, '[YT Enhanced]'),
  36. error: consoleError.bind(console, '[YT Enhanced]'),
  37. warn: consoleWarn.bind(console, '[YT Enhanced]'),
  38. info: consoleInfo.bind(console, '[YT Enhanced]'),
  39. debug: consoleDebug.bind(console, '[YT Enhanced]'),
  40. trace(...args) {
  41. consoleGroupCollapsed.bind(console, '[YT Enhanced]')(...args);
  42. consoleTrace(...args);
  43. consoleGroupEnd();
  44. },
  45. group: consoleGroup.bind(console, '[YT Enhanced]'),
  46. groupCollapsed: consoleGroupCollapsed.bind(console, '[YT Enhanced]'),
  47. groupEnd: consoleGroupEnd.bind(console)
  48. };
  49.  
  50. function defineReadonlyProperty(target, key, value, enumerable = true) {
  51. Object.defineProperty(target, key, {
  52. get() {
  53. return value;
  54. },
  55. set: o$1,
  56. configurable: false, // Make it harder to change
  57. enumerable
  58. });
  59. }
  60.  
  61. // Simple template literal tag for CSS readability
  62. function e(r, ...t) {
  63. return r.reduce((e, r, n) => e + r + (t[n] ?? ""), "")
  64. }
  65.  
  66. // --- Feature Modules ---
  67.  
  68. // 1. Disable AV1 Codec
  69. const disableAV1 = {
  70. name: 'disable-av1',
  71. description: 'Prevent YouTube from using AV1 codec',
  72. apply() {
  73. try {
  74. const originalCanPlayType = HTMLMediaElement.prototype.canPlayType;
  75. if (HTMLMediaElement && typeof originalCanPlayType === 'function') {
  76. HTMLMediaElement.prototype.canPlayType = function(type) {
  77. if (type && type.includes('av01')) {
  78. logger.info('AV1 canPlayType blocked:', type);
  79. return '';
  80. }
  81. return originalCanPlayType.call(this, type);
  82. };
  83. } else {
  84. logger.warn('HTMLMediaElement.prototype.canPlayType not found or not a function.');
  85. }
  86. const originalIsTypeSupported = unsafeWindow.MediaSource?.isTypeSupported;
  87. if (typeof originalIsTypeSupported === 'function') {
  88. unsafeWindow.MediaSource.isTypeSupported = function(type) {
  89. if (type && type.includes('av01')) {
  90. logger.info('AV1 isTypeSupported blocked:', type);
  91. return false;
  92. }
  93. return originalIsTypeSupported.call(this, type);
  94. };
  95. } else {
  96. logger.warn('MediaSource.isTypeSupported not found or not a function, cannot block AV1 via MediaSource.');
  97. }
  98. logger.log(this.name, 'applied');
  99. } catch (err) {
  100. logger.error('Error applying', this.name, err);
  101. }
  102. }
  103. };
  104.  
  105. // 2. Disable WebRTC
  106. const noWebRTC = {
  107. name: 'no-webrtc',
  108. description: 'Disable WebRTC Peer Connections',
  109. apply() {
  110. try {
  111. const rtcPcNames = [];
  112. if ('RTCPeerConnection' in unsafeWindow) rtcPcNames.push('RTCPeerConnection');
  113. if ('webkitRTCPeerConnection' in unsafeWindow) rtcPcNames.push('webkitRTCPeerConnection');
  114. if ('mozRTCPeerConnection' in unsafeWindow) rtcPcNames.push('mozRTCPeerConnection');
  115. const rtcDcNames = [];
  116. if ('RTCDataChannel' in unsafeWindow) rtcDcNames.push('RTCDataChannel');
  117. if ('webkitRTCDataChannel' in unsafeWindow) rtcDcNames.push('webkitRTCDataChannel');
  118. if ('mozRTCDataChannel' in unsafeWindow) rtcDcNames.push('mozRTCDataChannel');
  119. class MockDataChannel {close = o$1; send = o$1; addEventListener = o$1; removeEventListener = o$1; onbufferedamountlow = null; onclose = null; onerror = null; onmessage = null; onopen = null; get bufferedAmount() { return 0; } get id() { return null; } get label() { return ''; } get maxPacketLifeTime() { return null; } get maxRetransmits() { return null; } get negotiated() { return false; } get ordered() { return true; } get protocol() { return ''; } get readyState() { return 'closed'; } get reliable() { return false; } get binaryType() { return 'blob'; } set binaryType(val) {} get bufferedAmountLowThreshold() { return 0; } set bufferedAmountLowThreshold(val) {} toString() { return '[object RTCDataChannel]'; }}
  120. class MockRTCSessionDescription {type; sdp; constructor(init){ this.type = init?.type ?? 'offer'; this.sdp = init?.sdp ?? ''; } toJSON() { return { type: this.type, sdp: this.sdp }; } toString() { return '[object RTCSessionDescription]'; }}
  121. const mockedRtcSessionDescription = new MockRTCSessionDescription();
  122. class MockRTCPeerConnection {createDataChannel() { return new MockDataChannel(); } close = o$1; createOffer = noopNeverResolvedPromise; setLocalDescription = async () => {}; setRemoteDescription = async () => {}; addEventListener = o$1; removeEventListener = o$1; addIceCandidate = async () => {}; getConfiguration = () => ({}); getReceivers = () => []; getSenders = () => []; getStats = () => Promise.resolve(new Map()); getTransceivers = () => []; addTrack = () => null; removeTrack = o$1; addTransceiver = () => null; setConfiguration = o$1; get localDescription() { return mockedRtcSessionDescription; } get remoteDescription() { return mockedRtcSessionDescription; } get currentLocalDescription() { return mockedRtcSessionDescription; } get pendingLocalDescription() { return mockedRtcSessionDescription; } get currentRemoteDescription() { return mockedRtcSessionDescription; } get pendingRemoteDescription() { return mockedRtcSessionDescription; } get canTrickleIceCandidates() { return null; } get connectionState() { return 'disconnected'; } get iceConnectionState() { return 'disconnected'; } get iceGatheringState() { return 'complete'; } get signalingState() { return 'closed'; } onconnectionstatechange = null; ondatachannel = null; onicecandidate = null; onicecandidateerror = null; oniceconnectionstatechange = null; onicegatheringstatechange = null; onnegotiationneeded = null; onsignalingstatechange = null; ontrack = null; createAnswer = noopNeverResolvedPromise; toString() { return '[object RTCPeerConnection]'; }}
  123. for (const rtc of rtcPcNames) defineReadonlyProperty(unsafeWindow, rtc, MockRTCPeerConnection);
  124. for (const dc of rtcDcNames) defineReadonlyProperty(unsafeWindow, dc, MockDataChannel);
  125. defineReadonlyProperty(unsafeWindow, 'RTCSessionDescription', MockRTCSessionDescription);
  126. logger.log(this.name, 'applied');
  127. } catch (err) {
  128. logger.error('Error applying', this.name, err);
  129. }
  130. }
  131. };
  132.  
  133. // 3. Player Video Fit
  134. const playerVideoFit = {
  135. name: 'player-video-fit',
  136. description: 'Adds a toggle for video fit mode (cover/contain)',
  137. apply() {
  138. try {
  139. GM_addStyle(e`body[video-fit-mode-enabled] .html5-video-player video.video-stream, body[video-fit-mode-enabled] .html5-video-player .html5-main-video { object-fit: cover !important; } .ytp-settings-menu .ytp-menuitem[aria-haspopup="false"][role="menuitemcheckbox"] { justify-content: space-between; } .ytp-settings-menu .ytp-menuitem-label { flex-grow: 1; margin-right: 10px; } .ytp-menuitem-toggle-checkbox { margin: 0 !important; height: 100%; display: flex; align-items: center; }`);
  140. let fitModeEnabled = localStorage.getItem('yt-enhanced-video-fit') === 'true';
  141. function toggleMode(enabled) { fitModeEnabled = enabled; if (enabled) { document.body.setAttribute('video-fit-mode-enabled', ''); localStorage.setItem('yt-enhanced-video-fit', 'true'); } else { document.body.removeAttribute('video-fit-mode-enabled'); localStorage.setItem('yt-enhanced-video-fit', 'false'); } }
  142. function injectButtonLogic() { const observer = new MutationObserver((mutationsList, obs) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { const settingsMenu = document.querySelector('.ytp-settings-menu'); const panelMenu = settingsMenu?.querySelector('.ytp-panel-menu'); if (settingsMenu && panelMenu && !panelMenu.querySelector('#ytp-fit-mode-toggle')) { const style = window.getComputedStyle(settingsMenu); if (style.display !== 'none') { logger.debug('Settings menu opened, attempting to inject button.'); addButtonToMenu(panelMenu);}}}}}); const player = document.getElementById('movie_player'); if (player) { observer.observe(player, { childList: true, subtree: true }); logger.log('MutationObserver attached to player for settings menu.'); } else { setTimeout(() => { const playerRetry = document.getElementById('movie_player'); if (playerRetry) { observer.observe(playerRetry, { childList: true, subtree: true }); logger.log('MutationObserver attached to player after retry.'); } else { logger.warn('Player element not found for MutationObserver, Fit Mode button might not appear.'); }}, 2000); } const initialPanelMenu = document.querySelector('.ytp-settings-menu .ytp-panel-menu'); if (initialPanelMenu && !initialPanelMenu.querySelector('#ytp-fit-mode-toggle')) { const style = window.getComputedStyle(initialPanelMenu.closest('.ytp-settings-menu')); if (style.display !== 'none') { addButtonToMenu(initialPanelMenu); }} if (fitModeEnabled) { document.body.setAttribute('video-fit-mode-enabled', ''); }}
  143. function addButtonToMenu(panelMenu) { if (!panelMenu || panelMenu.querySelector('#ytp-fit-mode-toggle')) return; try { const newItem = document.createElement('div'); newItem.className = 'ytp-menuitem'; newItem.setAttribute('role', 'menuitemcheckbox'); newItem.setAttribute('aria-checked', fitModeEnabled.toString()); newItem.id = 'ytp-fit-mode-toggle'; newItem.tabIndex = 0; const label = document.createElement('div'); label.className = 'ytp-menuitem-label'; label.textContent = '裁切模式 (Fit Mode)'; const content = document.createElement('div'); content.className = 'ytp-menuitem-content'; content.innerHTML = `<div class="ytp-menuitem-toggle-checkbox"> ${fitModeEnabled ? '☑' : '☐'} </div>`; newItem.appendChild(label); newItem.appendChild(content); newItem.addEventListener('click', (evt) => { evt.stopPropagation(); const newState = !fitModeEnabled; toggleMode(newState); newItem.setAttribute('aria-checked', newState.toString()); content.innerHTML = `<div class="ytp-menuitem-toggle-checkbox"> ${newState ? '☑' : '☐'} </div>`; }); const qualityItem = Array.from(panelMenu.children).find(el => el.textContent.includes('Quality') || el.textContent.includes('画质')); if (qualityItem) { panelMenu.insertBefore(newItem, qualityItem.nextSibling); } else { const loopItem = Array.from(panelMenu.children).find(el => el.textContent.includes('Loop') || el.textContent.includes('循环播放')); if (loopItem) { panelMenu.insertBefore(newItem, loopItem); } else { const statsItem = Array.from(panelMenu.children).find(el => el.textContent.includes('Stats for nerds') || el.textContent.includes('详细统计信息')); if (statsItem) { panelMenu.insertBefore(newItem, statsItem); } else { panelMenu.appendChild(newItem); }}} logger.log('Fit Mode button injected.'); } catch (err) { logger.error("Error injecting Fit Mode button:", err); }}
  144. if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', injectButtonLogic); } else { injectButtonLogic(); }
  145. logger.log(this.name, 'applied');
  146. } catch (err) {
  147. logger.error('Error applying', this.name, err);
  148. }
  149. }
  150. };
  151.  
  152. // 4. Remove Black Backdrop Filter
  153. const removeBlackBackdropFilter = {
  154. name: 'remove-black-backdrop-filter',
  155. description: 'Removes potential site-wide grayscale filters',
  156. apply() {
  157. try { GM_addStyle(e`html, body { filter: none !important; -webkit-filter: none !important; }`); logger.log(this.name, 'applied'); } catch (err) { logger.error('Error applying', this.name, err); }
  158. }
  159. };
  160.  
  161. // 5. Remove Useless URL Parameters
  162. const removeUselessUrlParams = {
  163. name: 'remove-useless-url-params',
  164. description: 'Clean URLs from tracking parameters',
  165. apply() {
  166. try {
  167. const youtubeUselessUrlParams = [ 'si', 'pp', 'feature', 'gclid', 'dclid', 'fbclid', 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'oac', '_hsenc', '_hsmi', 'mc_eid', 'mc_cid', ];
  168. function removeTracking(url) { if (!url) return url; let urlObj; try { if (typeof url === 'string' && (url.startsWith('/') || url.startsWith('./') || url.startsWith('../'))) { urlObj = new URL(url, unsafeWindow.location.href); } else if (typeof url === 'string') { urlObj = new URL(url); } else if (url instanceof URL){ urlObj = url; } else { logger.warn('Invalid URL type for removeTracking:', url); return url; } if (!urlObj.search) return urlObj.href; const params = urlObj.searchParams; let changed = false; const keysToDelete = []; for (const key of params.keys()) { for (const item of youtubeUselessUrlParams) { let match = false; if (typeof item === 'string') { if (item === key) match = true; } else if (item instanceof RegExp && item.test(key)) { match = true; } if (match) { keysToDelete.push(key); break; }}} if (keysToDelete.length > 0) { keysToDelete.forEach(key => params.delete(key)); changed = true; } return changed ? urlObj.href : (typeof url === 'string' ? url : url.href); } catch (err) { if (err instanceof TypeError && err.message.includes("Invalid URL")) { return url; } logger.error('Failed to remove useless urlParams for:', url, err); return (typeof url === 'string' ? url : url?.href ?? ''); }}
  169. const initialHref = unsafeWindow.location.href; const cleanedHref = removeTracking(initialHref); if (initialHref !== cleanedHref) { logger.log('Initial URL cleaned:', initialHref, '->', cleanedHref); try { unsafeWindow.history.replaceState(unsafeWindow.history.state, '', cleanedHref); } catch (histErr) { logger.error("Failed to replaceState for initial URL:", histErr); }}
  170. const originalPushState = unsafeWindow.history.pushState; unsafeWindow.history.pushState = function(state, title, url) { const cleaned = removeTracking(url); if (url && url !== cleaned) { logger.log('pushState URL cleaned:', url, '->', cleaned); } try { return originalPushState.call(unsafeWindow.history, state, title, cleaned ?? url); } catch (pushErr) { logger.error("Error in hooked pushState:", pushErr); return originalPushState.call(unsafeWindow.history, state, title, url); }};
  171. const originalReplaceState = unsafeWindow.history.replaceState; unsafeWindow.history.replaceState = function(state, title, url) { const cleaned = removeTracking(url); if (url && url !== cleaned) { logger.log('replaceState URL cleaned:', url, '->', cleaned); } try { return originalReplaceState.call(unsafeWindow.history, state, title, cleaned ?? url); } catch (replaceErr) { logger.error("Error in hooked replaceState:", replaceErr); return originalReplaceState.call(unsafeWindow.history, state, title, url); }};
  172. logger.log(this.name, 'applied');
  173. } catch (err) {
  174. logger.error('Error applying', this.name, err);
  175. }
  176. }
  177. };
  178.  
  179. // 6. Use System Fonts
  180. const useSystemFonts = {
  181. name: 'use-system-fonts',
  182. description: 'Force system default fonts instead of YouTube specific fonts',
  183. apply() {
  184. try { GM_addStyle(e`html, body, #masthead, #content, ytd-app, tp-yt-app-drawer, #guide, input, button, textarea, select, .ytd-video-primary-info-renderer, .ytd-video-secondary-info-renderer, #comments, #comment, .ytd-rich-grid-media .ytd-rich-item-renderer #video-title, .ytp-tooltip-text, .ytp-menuitem-label, .ytp-title-text { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important; }`); logger.log(this.name, 'applied'); } catch (err) { logger.error('Error applying', this.name, err); }
  185. }
  186. };
  187.  
  188. // 7. 6 Thumbnails Per Row (ADAPTIVE COLUMNS & FONT SIZES)
  189. const sixThumbs = {
  190. name: 'six-thumbnails-per-row',
  191. description: 'Sets YouTube grid items to 6 per row (adapts to 5 on smaller screens), with adaptive font sizes and fixes channel page layout',
  192. apply() {
  193. try {
  194. GM_addStyle(e`
  195. /* --- BASE STYLES FOR RICH GRID --- */
  196. ytd-rich-grid-renderer {
  197. /* Default to 6 items, will be overridden by media query if needed */
  198. --ytd-rich-grid-items-per-row: 6 !important;
  199. /* Default max-width for 6 items */
  200. max-width: calc(
  201. (var(--ytd-rich-grid-item-max-width, 360px) * 6) + /* 6 items */
  202. (var(--ytd-rich-grid-item-margin, 16px) * 5) /* 5 gaps */
  203. ) !important;
  204. margin-left: auto !important;
  205. margin-right: auto !important;
  206. width: 100%;
  207. }
  208.  
  209. /* Ensure parent containers for grids also propagate the items per row variable */
  210. ytd-two-column-browse-results-renderer[is-grid] #primary #contents.ytd-section-list-renderer > *.ytd-section-list-renderer,
  211. ytd-browse #primary #contents.ytd-section-list-renderer > *.ytd-section-list-renderer:has(ytd-rich-grid-renderer),
  212. ytd-browse[page-subtype="subscriptions"] #contents.ytd-section-list-renderer {
  213. --ytd-rich-grid-items-per-row: 6 !important; /* Default to 6, overridden below */
  214. }
  215.  
  216.  
  217. /* Wider grid for specific top-level pages (Home, Subscriptions, Search) */
  218. ytd-browse[page-subtype="home"] ytd-rich-grid-renderer,
  219. ytd-browse[page-subtype="subscriptions"] ytd-rich-grid-renderer,
  220. ytd-search ytd-two-column-search-results-renderer ytd-section-list-renderer > #contents > ytd-item-section-renderer > #contents > ytd-rich-grid-renderer {
  221. width: calc(100vw - var(--ytd-guide-width, 240px) - (var(--ytd-masthead-shell-header-horizontal-padding, 24px) * 2) - 2px) !important;
  222. }
  223.  
  224. /* Inner contents of the grid */
  225. ytd-rich-grid-renderer > #contents.ytd-rich-grid-renderer {
  226. width: 100% !important;
  227. max-width: 100% !important;
  228. margin: 0 !important;
  229. }
  230.  
  231. /* Shelf renderers */
  232. ytd-shelf-renderer {
  233. --ytd-shelf-items-per-row: 6 !important;
  234. }
  235.  
  236. /* --- DEFAULT FONT & ELEMENT STYLING FOR GRID ITEMS --- */
  237. ytd-rich-item-renderer #video-title,
  238. ytd-grid-video-renderer #video-title {
  239. font-size: var(--ytd-font-title-sm_-_font-size, 1.6rem);
  240. line-height: var(--ytd-font-title-sm_-_line-height, 2.2rem);
  241. max-height: calc(var(--ytd-font-title-sm_-_line-height, 2.2rem) * 2) !important;
  242. -webkit-line-clamp: 2 !important;
  243. display: -webkit-box !important;
  244. -webkit-box-orient: vertical !important;
  245. overflow: hidden !important;
  246. text-overflow: ellipsis !important;
  247. white-space: normal !important;
  248. }
  249.  
  250. ytd-rich-grid-media #metadata-line,
  251. ytd-grid-video-renderer #metadata-line {
  252. font-size: var(--ytd-font-body-1_-_font-size, 1.3rem);
  253. line-height: var(--ytd-font-body-1_-_line-height, 1.8rem);
  254. }
  255. ytd-rich-grid-media #channel-name .ytd-channel-name,
  256. ytd-grid-video-renderer #channel-name .ytd-channel-name,
  257. ytd-channel-name a, ytd-channel-name .ytd-channel-name {
  258. font-size: var(--ytd-font-body-1_-_font-size, 1.3rem) !important;
  259. }
  260. ytd-rich-grid-media #avatar-link.ytd-rich-grid-media,
  261. ytd-grid-video-renderer #avatar-link {
  262. width: var(--ytd-rich-grid-channel-avatar-size, 36px);
  263. height: var(--ytd-rich-grid-channel-avatar-size, 36px);
  264. margin-right: var(--ytd-rich-grid-avatar-margin, 12px);
  265. flex-shrink: 0; /* Prevent avatar from shrinking if text is long */
  266. }
  267.  
  268. /* --- MEDIA QUERY FOR MEDIUM SCREENS (e.g., slightly narrower desktops, larger tablets) --- */
  269. @media (max-width: 1600px) {
  270. ytd-rich-item-renderer #video-title,
  271. ytd-grid-video-renderer #video-title {
  272. font-size: 1.3rem !important;
  273. line-height: 1.7rem !important;
  274. max-height: calc(1.7rem * 2) !important;
  275. }
  276.  
  277. ytd-rich-grid-media #metadata-line,
  278. ytd-grid-video-renderer #metadata-line,
  279. ytd-rich-grid-media #channel-name .ytd-channel-name,
  280. ytd-grid-video-renderer #channel-name .ytd-channel-name,
  281. ytd-channel-name a, ytd-channel-name .ytd-channel-name {
  282. font-size: 1.15rem !important;
  283. line-height: 1.5rem !important;
  284. }
  285.  
  286. ytd-rich-grid-media #avatar-link.ytd-rich-grid-media,
  287. ytd-grid-video-renderer #avatar-link {
  288. width: 28px !important;
  289. height: 28px !important;
  290. margin-right: 8px !important;
  291. }
  292. }
  293.  
  294. /* --- MEDIA QUERY FOR SMALLER SCREENS (e.g., 14" MacBook Pro default, etc.) --- */
  295. @media (max-width: 1550px) {
  296. ytd-rich-grid-renderer,
  297. ytd-two-column-browse-results-renderer[is-grid] #primary #contents.ytd-section-list-renderer > *.ytd-section-list-renderer,
  298. ytd-browse #primary #contents.ytd-section-list-renderer > *.ytd-section-list-renderer:has(ytd-rich-grid-renderer),
  299. ytd-browse[page-subtype="subscriptions"] #contents.ytd-section-list-renderer {
  300. --ytd-rich-grid-items-per-row: 5 !important; /* Change to 5 items */
  301. }
  302.  
  303. ytd-rich-grid-renderer {
  304. max-width: calc(
  305. (var(--ytd-rich-grid-item-max-width, 360px) * 5) + /* 5 items */
  306. (var(--ytd-rich-grid-item-margin, 16px) * 4) /* 4 gaps */
  307. ) !important;
  308. }
  309. }
  310. `);
  311. logger.log(this.name, 'applied with adaptive columns and font sizes');
  312. } catch (err) {
  313. logger.error('Error applying', this.name, err);
  314. }
  315. }
  316. };
  317.  
  318. // 8. Remove Shorts
  319. const removeShorts = {
  320. name: 'remove-shorts',
  321. description: 'Hides YouTube Shorts elements from the UI',
  322. apply() {
  323. try {
  324. GM_addStyle(e`
  325. ytd-guide-entry-renderer:has(a#endpoint[title='Shorts']),
  326. ytd-guide-entry-renderer:has(yt-icon path[d^='M10 14.14V9.86']),
  327. ytd-mini-guide-entry-renderer[aria-label='Shorts'] { display: none !important; }
  328. ytd-reel-shelf-renderer, ytd-rich-shelf-renderer[is-shorts] { display: none !important; }
  329. ytd-grid-video-renderer:has(ytd-thumbnail-overlay-time-status-renderer[overlay-style='SHORTS']),
  330. ytd-video-renderer:has(ytd-thumbnail-overlay-time-status-renderer[overlay-style='SHORTS']),
  331. ytd-rich-item-renderer:has(ytd-reel-item-renderer) { display: none !important; }
  332. tp-yt-paper-tab:has( > .tab-content > .tab-title:contains("Shorts")) { display: none !important; }
  333. ytd-rich-grid-renderer #title-container.ytd-rich-grid-renderer:has(h2 yt-formatted-string:contains("Shorts")) { display: none !important; }
  334. `);
  335. logger.log(this.name, 'applied');
  336. } catch (err) {
  337. logger.error('Error applying', this.name, err);
  338. }
  339. }
  340. };
  341.  
  342.  
  343. // --- Apply Features ---
  344. logger.log('Initializing YouTube Enhanced script...');
  345.  
  346. disableAV1.apply();
  347. noWebRTC.apply();
  348. removeUselessUrlParams.apply();
  349. sixThumbs.apply(); // This now includes the adaptive logic
  350. useSystemFonts.apply();
  351. removeBlackBackdropFilter.apply();
  352. removeShorts.apply();
  353. playerVideoFit.apply();
  354.  
  355. logger.log('YouTube Enhanced script initialization complete.');
  356.  
  357. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址