您需要先安装一个扩展,例如 篡改猴、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.5 // @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 (let i = 0; i < tabs.length; i++) { const tab = tabs[i]; const tabRenderer = tab.tabRenderer; if (tabRenderer) { const richGridRenderer = tabRenderer.content?.richGridRenderer; if (richGridRenderer && (!richGridRenderer.style || richGridRenderer.style == 'RICH_GRID_STYLE_SLIM')) { const logTabContext = logContext + ' tab ' + (tabRenderer.title ?? tabRenderer.tabIdentifier ?? i); // /feed/playlists page (and possibly other pages) have additional properties: // layoutSizing: 'RICH_GRID_LAYOUT_SIZING_COMPACT' // layoutType: 'RICH_GRID_LAYOUT_TYPE_COMPACT_LIST' // These need to be deleted when setting style, or otherwise, the page can hang. if (richGridRenderer.layoutSizing) { log(logTabContext, 'tabRenderer.content.richGridRenderer.layoutSizing:', richGridRenderer.layoutSizing, '=> (none)'); delete richGridRenderer.layoutSizing; } if (richGridRenderer.layoutType) { log(logTabContext, 'tabRenderer.content.richGridRenderer.layoutType:', richGridRenderer.layoutType, '=> (none)'); delete richGridRenderer.layoutType; } log(logTabContext, '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, 'at 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 computeFluidWidth(data, selectedTab, pageSubtype) // (formerly computeRichGridValue(pageSubtype)) to ensure returns false for home page // to match that of the subscription/channel pages. function setupYtdBrowse(ytdBrowse, logContext, errorFunc=error) { if (!ytdBrowse || !ytdBrowse.computeFluidWidth) { return errorFunc('unexpectedly could not find ytd-browse.computeFluidWidth'); } if (!ytdBrowse) { return errorFunc('unexpectedly could not find ytd-browse'); } if (ytdBrowse[symSetup]) { return; } debug('found ytd-browse', ytdBrowse, logContext); const origComputeFluidWidth = ytdBrowse.computeFluidWidth; ytdBrowse.computeFluidWidth = function(data, selectedTab, pageSubtype) { debug('computeFluidWidth', pageSubtype); if (pageSubtype === 'home') { return false; } return origComputeFluidWidth.call(this, pageSubtype); }; debug('ytd-app computeFluidWidth hook set up'); ytdBrowse[symSetup] = true; } // Note: Following didn't work to force ytd-browse's computeFluidWidth to return false for home page, // since in the EXPERIMENT_FLAGS.rich_grid_browse_compute_kill_switch=false case, // that function still ends up returning true for non-channel pages. // function setupExperimentFlags(logContext) { // const ytcfg = window.ytcfg; // if (ytcfg) { // const expFlags = ytcfg.get('EXPERIMENT_FLAGS'); // if (expFlags && expFlags.rich_grid_browse_compute_kill_switch !== false) { // log(logContext, 'ytcfg.EXPERIMENT_FLAGS.rich_grid_browse_compute_kill_switch:', // expFlags.rich_grid_browse_compute_kill_switch, '=>', false); // expFlags.rich_grid_browse_compute_kill_switch = false; // } // } // } // 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 computeFluidWidth. 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', () => {}); //setupExperimentFlags('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, 'at DOMContentLoaded ytInitialData'); //setupExperimentFlags('at DOMContentLoaded'); }); })();