您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Force YouTube to show compact grid (max 6 videos per row) rather than "slim" grid (max 3 videos per row)
当前为
// ==UserScript== // @name YouTube - Force Compact Grid (increases max # videos per row) // @namespace https://gist.github.com/lbmaian/8c6961584c0aebf41ee7496609f60bc3 // @version 0.3 // @description Force YouTube to show compact grid (max 6 videos per row) rather than "slim" grid (max 3 videos per row) // @author lbmaian // @match https://www.youtube.com/* // @exclude https://www.youtube.com/embed/* // @icon https://www.youtube.com/favicon.ico // @run-at document-start // @grant none // ==/UserScript== (function() { 'use strict'; const DEBUG = false; const logContext = '[YouTube - Force Compact Grid]'; var debug; if (DEBUG) { debug = function(...args) { console.debug(logContext, ...args); }; } else { debug = function() {}; } function log(...args) { console.log(logContext, ...args); } function info(...args) { console.info(logContext, ...args); } function warn(...args) { console.warn(logContext, ...args); } function error(...args) { console.error(logContext, ...args); } // Updates richGridRenderer data to coerce unknown/slim grid style to compact style. function updateResponseData(response, logContext) { const tabs = response?.contents?.twoColumnBrowseResultsRenderer?.tabs; if (DEBUG) { debug(logContext, 'contents.twoColumnBrowseResultsRenderer.tabs (snapshot)', window.structuredClone(tabs)); } if (tabs) { for (const tab of tabs) { const tabRenderer = tab.tabRenderer; if (tabRenderer) { const richGridRenderer = tabRenderer.content?.richGridRenderer; if (richGridRenderer && (!richGridRenderer.style || richGridRenderer.style == 'RICH_GRID_STYLE_SLIM')) { log(logContext, 'tab', tabRenderer.title ?? tabRenderer.tabIdentifier, 'tabRenderer.content.richGridRenderer.style:', richGridRenderer.style, '=> RICH_GRID_STYLE_COMPACT'); richGridRenderer.style = 'RICH_GRID_STYLE_COMPACT'; } } } } } // Note: Both of the following commented-out event listeners are too late: // ytd-app's own yt-page-data-fetched event listener (onYtPageDataFetched) already fires // by the time our own yt-page-data-fetched event listener fires, // and yt-navigate-finish fires after yt-page-data-fetched fires. // document.addEventListener('yt-page-data-fetched', evt => { // debug('Navigated to', evt.detail.pageData.url); // debug(evt); // updateResponseData(evt.detail.pageData.response, 'yt-page-data-fetched pageData.response'); // }); // document.addEventListener('yt-navigate-finish', evt => { // debug('Navigated to', evt.detail.response.url); // debug(evt); // updateResponseData(evt.detail.response.response, 'yt-navigate-finish response.response'); // }); const symSetup = Symbol(logContext + ' setup'); // yt-page-data-fetched event fires on both new page load and channel tab change. // Need to hook into ytd-app's ytd-app's own yt-page-data-fetched event listener (onYtPageDataFetched), // so that we can modify the data before that event listener fires. function setupYtdApp(ytdApp, logContext, errorFunc=error) { // ytd-app's prototype is initialized after the element is created, // so need to check that the onYtPageDataFetched method exists. if (!ytdApp || !ytdApp.onYtPageDataFetched) { return errorFunc('unexpectedly could not find ytd-app.onYtPageDataFetched'); } if (ytdApp[symSetup]) { return; } debug('found yt-App', ytdApp, logContext); const origOnYtPageDataFetched = ytdApp.onYtPageDataFetched; ytdApp.onYtPageDataFetched = function(evt, detail) { debug(evt); updateResponseData(evt.detail.pageData.response, 'yt-page-data-fetched pageData.response'); return origOnYtPageDataFetched.call(this, evt, detail); }; debug('ytd-app onYtPageDataFetched hook set up'); ytdApp[symSetup] = true; } // Need to hook into ytd-page-manager's attachPage to hook into ytd-browse. function setupYtdPageManager(ytdPageManager, logContext, errorFunc=error) { if (!ytdPageManager || !ytdPageManager.attachPage) { return errorFunc('unexpectedly could not find ytd-page-manager.attachPage'); } if (ytdPageManager[symSetup]) { return; } debug('found ytd-page-manager', ytdPageManager, logContext); const origAttachPage = ytdPageManager.attachPage; ytdPageManager.attachPage = function(page) { debug('attachPage', page); if (page.is === 'ytd-browse') { setupYtdBrowse(page, 'at ytd-page-manager.attachPage'); } return origAttachPage.call(this, page); }; debug('ytd-page-manager attachPage hook set up'); ytdPageManager[symSetup] = true; } // Need to hook into ytd-browse's computeRichGridValue to ensure it always returns false, // in particular changing the YT homepage grid to match that of the subscription/channel pages. function setupYtdBrowse(ytdBrowse, logContext, errorFunc=error) { if (!ytdBrowse || !ytdBrowse.computeRichGridValue) { return errorFunc('unexpectedly could not find ytd-browse.computeRichGridValue'); } if (ytdBrowse[symSetup]) { return; } debug('found ytd-browse', ytdBrowse, logContext); const origComputeRichGridValue = ytdBrowse.computeRichGridValue; ytdBrowse.computeRichGridValue = function(pageSubtype) { debug('computeRichGridValue', pageSubtype); if (pageSubtype === 'home') { return false; } return origComputeRichGridValue.call(this, pageSubtype); }; debug('ytd-app computeRichGridValue hook set up'); ytdBrowse[symSetup] = true; } // By the time ytd-page-manager's attached event fires, ytd-app both exists // and has its prototype initialized as needed in the above setup functions. // (This also fires sooner than a MutationObserver would find such a ytd-app.) // This is also the perfect hook for hooking into ytd-page-manager, // which in turn allows hooking into ytd-browse's computeRichGridValue. document.addEventListener('attached', evt => { const ytdApp = document.getElementsByTagName('ytd-app')[0]; debug(evt); setupYtdApp(ytdApp, 'at ytd-page-manager.attached'); const ytdPageManager = evt.srcElement; setupYtdPageManager(ytdPageManager, 'at ytd-page-manager.attached'); }); // In case, ytd-app somehow already exists at this point. const ytdApp = document.getElementsByTagName('ytd-app')[0]; setupYtdApp(ytdApp, 'at document-start', () => {}); // Note: updating ytInitialData may not be necessary, since yt-page-data-fetched also fires for new page load, // and in that case, the event's detail.pageData.response is the same object as ytInitialData, // but DOMContentLoaded sometimes fires before ytd-app's onYtPageDataFetched fires (or rather, before we can hook into it), // so this is done just in case. document.addEventListener('DOMContentLoaded', evt => { debug('ytInitialData', window.ytInitialData); updateResponseData(window.ytInitialData, 'ytInitialData'); }); })();