YouTube Grid Auto-Scroll & Search (Ultra Optimized - Instant)

Automatically scrolls to a specified text within a YouTube grid. Search box in masthead, search on button click only, stop button, animated border highlighting, and no results message. Improved robustness, accessibility, and user experience.

02.03.2025 itibariyledir. En son verisyonu görün.

// ==UserScript==
// @name         YouTube Grid Auto-Scroll & Search (Ultra Optimized - Instant)
// @match        https://www.youtube.com/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @version      2.3
// @description  Automatically scrolls to a specified text within a YouTube grid. Search box in masthead, search on button click only, stop button, animated border highlighting, and no results message. Improved robustness, accessibility, and user experience.
// @author       Your Name (with further optimization)
// @license      MIT
// @namespace    https://greasyfork.org/users/1435316
// ==/UserScript==

(function() {
    'use strict';

    let targetText = "";
    let searchBox;
    let isSearching = false;
    let searchInput;
    let searchButton;
    let stopButton;
    let observer;
    let searchTimeout;
    const SEARCH_TIMEOUT_MS = 20000; // 20 seconds
    const SCROLL_DELAY_MS = 750;
    const MAX_SEARCH_LENGTH = 255; // Prevent excessively long search strings

    GM_addStyle(`
        /* Existing CSS (with additions) */
        #floating-search-box {
            background-color: #222;
            padding: 5px;
            border: 1px solid #444;
            border-radius: 5px;
            display: flex;
            align-items: center;
            margin-left: 10px;
        }
        /* Responsive width for smaller screens */
        @media (max-width: 768px) {
            #floating-search-box input[type="text"] {
                width: 150px; /* Smaller width on smaller screens */
            }
        }

        #floating-search-box input[type="text"] {
            background-color: #333;
            color: #fff;
            border: 1px solid #555;
            padding: 3px 5px;
            border-radius: 3px;
            margin-right: 5px;
            width: 200px;
            height: 30px;
        }
        #floating-search-box input[type="text"]:focus {
            outline: none;
            border-color: #065fd4;
        }
        #floating-search-box button {
            background-color: #065fd4;
            color: white;
            border: none;
            padding: 3px 8px;
            border-radius: 3px;
            cursor: pointer;
            height: 30px;
        }
        #floating-search-box button:hover {
            background-color: #0549a8;
        }
        #floating-search-box button:focus {
            outline: none;
        }

        #stop-search-button {
            background-color: #aa0000; /* Red color */
        }
        #stop-search-button:hover {
             background-color: #800000;
        }

        .highlighted-text {
            position: relative; /* Needed for the border to be positioned correctly */
            z-index: 1;       /* Ensure the border is on top of other elements */
        }

        /* Creates the animated border effect */
        .highlighted-text::before {
            content: '';
            position: absolute;
            top: -2px;
            left: -2px;
            right: -2px;
            bottom: -2px;
            border: 2px solid transparent;
            border-radius: 8px;
            background: linear-gradient(to right, red, orange, yellow, green, blue, indigo, violet);
            background-size: 400% 400%;
            animation: gradientAnimation 5s ease infinite;
            z-index: -1;
        }
        /* Keyframes for the gradient animation */
        @keyframes gradientAnimation {
            0% {
                background-position: 0% 50%;
            }
            50% {
                background-position: 100% 50%;
            }
            100% {
                background-position: 0% 50%;
            }
        }

       /* Style for the error message */
        #search-error-message {
            color: red;
            font-weight: bold;
            padding: 5px;
            position: fixed; /* Fixed position */
            top: 50px;      /* Position below the masthead (adjust as needed)*/
            left: 50%;
            transform: translateX(-50%); /* Center horizontally */
            background-color: rgba(0, 0, 0, 0.8); /* Semi-transparent black */
            color: white;
            border-radius: 5px;
            z-index: 10000; /* Ensure it's on top */
            display: none;   /* Initially hidden */
         }

         /* Style for the no results message */
        #search-no-results-message {
            color: #aaa; /* Light gray */
            padding: 5px;
            position: fixed;
            top: 50px;  /* Same position as error message */
            left: 50%;
            transform: translateX(-50%);
            background-color: rgba(0, 0, 0, 0.8);
            border-radius: 5px;
            z-index: 10000;
            display: none;  /* Initially hidden */
         }

    `);


    // --- Create the Search Box ---
    function createSearchBox() {
        searchBox = document.createElement('div');
        searchBox.id = 'floating-search-box';
        searchBox.setAttribute('role', 'search'); // Add ARIA role for accessibility

        searchInput = document.createElement('input');
        searchInput.type = 'text';
        searchInput.placeholder = 'Search to scroll...';
        searchInput.value = GM_getValue('lastSearchTerm', '');
        searchInput.setAttribute('aria-label', 'Search within YouTube grid'); // ARIA label
        searchInput.maxLength = MAX_SEARCH_LENGTH; // Limit input length

        searchButton = document.createElement('button');
        searchButton.textContent = 'Search';
        searchButton.addEventListener('click', searchAndScroll);
        searchButton.setAttribute('aria-label', 'Start search'); // ARIA label

        stopButton = document.createElement('button');
        stopButton.textContent = 'Stop';
        stopButton.id = 'stop-search-button';
        stopButton.addEventListener('click', stopSearch);
        stopButton.setAttribute('aria-label', 'Stop search');  // ARIA label

        searchBox.appendChild(searchInput);
        searchBox.appendChild(searchButton);
        searchBox.appendChild(stopButton);

        const mastheadEnd = document.querySelector('#end.ytd-masthead');
        const buttonsContainer = document.querySelector('#end #buttons');

        if (mastheadEnd) {
            if(buttonsContainer){
                mastheadEnd.insertBefore(searchBox, buttonsContainer);
            } else{
                mastheadEnd.appendChild(searchBox);
            }
        } else {
            console.error("Could not find the YouTube masthead's end element.");
            showErrorMessage("Could not find the YouTube masthead. Search box placed at top of page.");
            document.body.insertBefore(searchBox, document.body.firstChild);
        }

        // Trigger search on load if text is present and the URL indicates a channel page
        if (searchInput.value.trim() !== "" && window.location.href.includes("/videos")) {
            searchAndScroll();
        }
    }


    // --- Show Error Message ---
    function showErrorMessage(message) {
        let errorDiv = document.getElementById('search-error-message');
        if (!errorDiv) {
            errorDiv = document.createElement('div');
            errorDiv.id = 'search-error-message';
            document.body.appendChild(errorDiv);
        }
        errorDiv.textContent = message;
        errorDiv.style.display = 'block';

        setTimeout(() => {
            errorDiv.style.display = 'none';
        }, 5000); // Hide after 5 seconds
    }
    // --- Show "No Results" Message ---
    function showNoResultsMessage() {
        let noResultsDiv = document.getElementById('search-no-results-message');
        if (!noResultsDiv) {
            noResultsDiv = document.createElement('div');
            noResultsDiv.id = 'search-no-results-message';
            noResultsDiv.textContent = "No matching results found.";  // Set the text
            document.body.appendChild(noResultsDiv);
        }
    noResultsDiv.style.display = 'block';
     // Hide the message after a few seconds
        setTimeout(() => {
            noResultsDiv.style.display = 'none';
        }, 5000);

    }

    // --- Stop Search Function ---
    function stopSearch() {
        if (observer) {
            observer.disconnect();
        }
        isSearching = false;
        clearTimeout(searchTimeout);
        const prevHighlighted = document.querySelector('.highlighted-text');
        if (prevHighlighted) {
            prevHighlighted.classList.remove('highlighted-text');
        }
    }


    // --- Optimized Search and Scroll Function ---
    function searchAndScroll() {
        if (isSearching) return;
        isSearching = true;
        clearTimeout(searchTimeout);

        if (observer) {
            observer.disconnect();
        }

        targetText = searchInput.value.trim().toLowerCase();
        if (!targetText) {
            isSearching = false;
            return;
        }

        GM_setValue('lastSearchTerm', targetText);
         // Remove previous highlights
        const prevHighlighted = document.querySelector('.highlighted-text');
          if (prevHighlighted) {
            prevHighlighted.classList.remove('highlighted-text');
            prevHighlighted.style.position = '';  //remove inline styles, if any
        }

        let foundMatch = false; // Keep track of whether a match was found

        observer = new IntersectionObserver((entries) => {
            for (const entry of entries) {
                if (entry.isIntersecting) {
                    const titleElement = entry.target.querySelector('#video-title');  // More reliable selector
                    if (titleElement && titleElement.textContent.toLowerCase().includes(targetText)) {
                       foundMatch = true;
                       entry.target.scrollIntoView({ behavior: 'auto', block: 'center' });
                       entry.target.classList.add('highlighted-text');
                       observer.disconnect();
                       isSearching = false;
                       clearTimeout(searchTimeout);
                       return;
                    }
                }
            }
        });

        //Observe all grid items.
        document.querySelectorAll('ytd-rich-grid-media').forEach(item => {
            observer.observe(item);
        });

        searchTimeout = setTimeout(() => {
           stopSearch();
            if (!foundMatch) {
             showNoResultsMessage(); //show no results message.
            }
        }, SEARCH_TIMEOUT_MS);

        setTimeout(() => {
        if (!document.querySelector('.highlighted-text')) {
            window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'auto' });
            setTimeout(() => {
                if (!isSearching) return;  // Check again in case stop was pressed
                isSearching = false;
                searchAndScroll(); // Recursive call
            }, SCROLL_DELAY_MS);
        } else {
             isSearching = false;
         }
        }, 100);
    }

    // --- Initialization ---
    createSearchBox();

})();