YouTube Grid Row Controller

Adds simple buttons to control items per row on Youtube's home feed, works for shorts and news sections too. Buttons can be hidden if needed.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         YouTube Grid Row Controller
// @namespace    https://github.com/HageFX-78
// @version      1.0
// @description  Adds simple buttons to control items per row on Youtube's home feed, works for shorts and news sections too. Buttons can be hidden if needed.
// @author       HageFX78
// @license      MIT
// @match        *://www.youtube.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function () {
    'use strict';

    // Configurable options
    const hideControls = GM_getValue('hideControls', false); // set true to hide UI controls, it will use the default values instead

    const transparentButtons = GM_getValue('transparentButtons', false); // set true to make the buttons transparent and less intrusive, only applies if hideControls is false

    const defaultSettingValue = {
        // Default values mainly used when if you want to hide the buttons, change the values to your liking
        content: 4,
        news: 5,
        shorts: 6,
    };

    let currentSettingValues = {
        content: GM_getValue('itemPerRow', defaultSettingValue.content),
        news: GM_getValue('newsPerRow', defaultSettingValue.news),
        shorts: GM_getValue('shortsPerRow', defaultSettingValue.shorts),
    };

    // Styles
    const style = (css) => {
        const el = document.createElement('style');
        el.textContent = css;
        document.head.appendChild(el);
        return el;
    };

    // Some of it maybe irrelevant after so long, will cleanup someday...
    style(`
		${hideControls ? '' : '#chips-content{width: 92% !important;}'}

		.justify-left-custom { justify-content: left !important; }
        .justify-center-custom { justify-content: center !important; }

        ytd-rich-item-renderer[rendered-from-rich-grid][is-in-first-column] { margin-left: calc(var(--ytd-rich-grid-item-margin) / 2) !important; }
		
		ytd-rich-item-renderer[hidden][is-responsive-grid], [is-slim-media]{ display: block !important; }

		ytd-rich-item-renderer{ margin-bottom: var(--ytd-rich-grid-row-margin) !important; }

		.button-container.ytd-rich-shelf-renderer { display: none !important;  }
		
		#dismissible.ytd-rich-shelf-renderer {
			padding-bottom: 0 !important;
			border-bottom: none !important;
		}
            
        #selected-chip-content{
            width: 0% !important;
        }

        #spacer.ytd-shelf-renderer {
            flex: 9 !important; /* Spacing gets weird in subscription feed page */
        }

        ytd-feed-filter-chip-bar-renderer[frosted-glass-mode=with-chipbar] #chips-wrapper.ytd-feed-filter-chip-bar-renderer {
            flex-direction: row;
        }
		.itemPerRowControl {
            display: flex;
            justify-content: right;
            align-items: center;

            z-index: 2025;
            flex: 1;         
            gap: 10px;
            box-sizing: border-box;
            user-select: none;
			width: 8%;
        }

        .itemPerRowControl button {

            border: none;
            color: var(--yt-spec-text-primary);
            background-color:${transparentButtons ? 'transparent' : 'var(--yt-spec-badge-chip-background)'};
            font-size: 24px;
            
            text-align: center;
            display: inline-block;

            height: 30px;
            aspect-ratio: 1/1;
            border-radius: 50%;
        }

        .itemPerRowControl button:hover {
            background-color: var(--yt-spec-button-chip-background-hover);
            cursor: pointer;
        }
	`);

    const dynamicStyle = style('');

    function updatePageLayout() {
        dynamicStyle.textContent = `
			ytd-rich-grid-renderer {
				--ytd-rich-grid-items-per-row: ${hideControls ? defaultSettingValue.content : currentSettingValues.content} !important;
			}
			ytd-rich-shelf-renderer:not([is-shorts]) {
				--ytd-rich-grid-items-per-row: ${hideControls ? defaultSettingValue.news : currentSettingValues.news} !important;
			}
			ytd-rich-shelf-renderer[is-shorts] {
				--ytd-rich-grid-slim-items-per-row: ${hideControls ? defaultSettingValue.shorts : currentSettingValues.shorts} !important;
                --ytd-rich-grid-items-per-row: ${hideControls ? defaultSettingValue.shorts : currentSettingValues.shorts} !important;
			}
		`;
    }

    function saveValues() {
        GM_setValue('itemPerRow', currentSettingValues.content);
        GM_setValue('newsPerRow', currentSettingValues.news);
        GM_setValue('shortsPerRow', currentSettingValues.shorts);
    }

    function updateAndSave() {
        updatePageLayout();
        saveValues();
    }

    function isCreatorPage() {
        return location.pathname.startsWith('/@');
    }

    function initGlobalWatcher() {
        const targets = [
            {
                selector: '#chips-wrapper',
                type: 'content',
                place: (anchor, control) => anchor.appendChild(control),
            },
            {
                selector: 'ytd-rich-section-renderer #menu-container',
                type: (node) => (node.closest('ytd-rich-section-renderer')?.querySelector('[is-shorts]') ? 'shorts' : 'news'),
                place: (anchor, control) => anchor.parentNode.insertBefore(control, anchor),
            },
            {
                selector: 'ytd-shelf-renderer #title-container.style-scope.ytd-shelf-renderer',
                type: 'content',
                place: (anchor, control) => anchor.appendChild(control),
            },
        ];

        scanExistingAnchors(targets); // Some elements load before observer can be hooked, like the #chips

        const observer = new MutationObserver((muts) => {
            for (const m of muts) {
                for (const node of m.addedNodes) {
                    if (node.nodeType !== 1) continue;

                    for (const t of targets) {
                        const anchor = node.matches(t.selector) ? node : node.querySelector?.(t.selector);

                        if (anchor) tryAttachControl(anchor, t);
                    }
                }
            }
        });

        observer.observe(document.documentElement, { subtree: true, childList: true });
    }

    function tryAttachControl(anchor, t) {
        if (!anchor) return;
        if (isCreatorPage()) return;

        // Prevent duplicates
        if (anchor.parentNode?.querySelector?.('.itemPerRowControl')) return;

        const type = typeof t.type === 'function' ? t.type(anchor) : t.type;
        const control = createControlDivRaw(type);

        // CENTER for #chips-wrapper and the shelf title container
        if (t.selector === '#chips-wrapper') {
            control.classList.add('justify-left-custom');
        } else if (t.selector.startsWith('ytd-shelf-renderer')) {
            control.classList.add('justify-center-custom');
        }

        t.place(anchor, control);
    }

    function createControlDivRaw(type) {
        const controlDiv = document.createElement('div');
        controlDiv.classList.add('style-scope', 'ytd-rich-grid-renderer', 'itemPerRowControl');

        ['-', '+'].forEach((symbol) => {
            const btn = document.createElement('button');
            btn.textContent = symbol;

            btn.addEventListener('click', () => {
                if (symbol === '+') currentSettingValues[type]++;
                else if (currentSettingValues[type] > 1) currentSettingValues[type]--;

                updateAndSave();
            });

            controlDiv.appendChild(btn);
        });

        return controlDiv;
    }

    function scanExistingAnchors(targets) {
        for (const t of targets) {
            document.querySelectorAll(t.selector).forEach((anchor) => {
                tryAttachControl(anchor, t);
            });
        }
    }

    function setupGMMenu() {
        function rebuildButtonStyles(newVal) {
            document.querySelectorAll('.itemPerRowControl button').forEach((btn) => {
                btn.style.backgroundColor = newVal ? 'transparent' : 'var(--yt-spec-badge-chip-background)';
            });
        }

        function applyHideControls(newVal) {
            const controls = document.querySelectorAll('.itemPerRowControl');
            controls.forEach((c) => {
                c.style.display = newVal ? 'none' : 'flex';
            });

            // force layout update
            updatePageLayout();
        }

        if (typeof GM_registerMenuCommand === 'function') {
            GM_registerMenuCommand(`Reset Values`, () => {
                GM_setValue('itemPerRow', defaultSettingValue.content);
                GM_setValue('newsPerRow', defaultSettingValue.news);
                GM_setValue('shortsPerRow', defaultSettingValue.shorts);

                currentSettingValues = { ...defaultSettingValue };
                updatePageLayout();
            });

            GM_registerMenuCommand(`Toggle hideControls [ ${hideControls} ]`, () => {
                let newVal = !GM_getValue('hideControls', false);
                GM_setValue('hideControls', newVal);
                applyHideControls(newVal);
            });

            GM_registerMenuCommand(`Toggle transparentButtons [ ${transparentButtons} ]`, () => {
                let newVal = !GM_getValue('transparentButtons', false);
                GM_setValue('transparentButtons', newVal);
                rebuildButtonStyles(newVal);
            });
        }
    }

    // ----------------------------------- Main Execution -----------------------------------
    setupGMMenu();
    updatePageLayout();

    if (!hideControls) initGlobalWatcher();
})();