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

Automatically scrolls to the *next* matching video title, highlighting all matches. Search box, search on click, stop, animated border, no results. Handles dynamic loading.

目前为 2025-03-02 提交的版本,查看 最新版本

// ==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.7
// @description  Automatically scrolls to the *next* matching video title, highlighting all matches. Search box, search on click, stop, animated border, no results. Handles dynamic loading.
// @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 searchTimeout;
    const SEARCH_TIMEOUT_MS = 20000; // 20 seconds
    const SCROLL_DELAY_MS = 750;
    const MAX_SEARCH_LENGTH = 255;
    let lastFoundIndex = -1; // Index of the last highlighted element
    let lastScrollHeight = 0; // Keep track of scroll height to detect end of content

    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;  /* Transparent border to start */
          border-radius: 8px;          /* Rounded corners */
          background: linear-gradient(to right, red, orange, yellow, green, blue, indigo, violet); /* Rainbow gradient */
          background-size: 400% 400%;  /* Make the gradient larger than the element */
          animation: gradientAnimation 5s ease infinite; /* Animate the background position */
          z-index: -1;                /* Behind the content */
        }
        /* 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;
            top: 50px;
            left: 50%;
            transform: translateX(-50%);
            background-color: rgba(0, 0, 0, 0.8);
            color: white;
            border-radius: 5px;
            z-index: 10000;
            display: none;
         }

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

   // --- 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); //fallback
        }

         // Trigger search on load if text is present AND we're on a videos 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.";
            document.body.appendChild(noResultsDiv);
        }
        noResultsDiv.style.display = 'block';

        setTimeout(() => {
            noResultsDiv.style.display = 'none';
        }, 5000);
    }

    // --- Stop Search Function ---
    function stopSearch() {
         if (observer) {
            observer.disconnect();
            observer = null; // Ensure observer is nulled out
        }
        isSearching = false;
        clearTimeout(searchTimeout);
         // Reset the index when a new search starts
        lastFoundIndex = -1;
    }
  // --- Optimized Search and Scroll Function ---
function searchAndScroll() {
    if (isSearching) return;
    isSearching = true;
    clearTimeout(searchTimeout);

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

    GM_setValue('lastSearchTerm', targetText);

    // Get all *visible* media elements
    const mediaElements = Array.from(document.querySelectorAll('ytd-rich-grid-media:not([style*="display: none"])'));

    // Find the next matching element, starting from lastFoundIndex + 1
    let nextMatchIndex = -1;
    for (let i = lastFoundIndex + 1; i < mediaElements.length; i++) {
        const titleElement = mediaElements[i].querySelector('#video-title');
        if (titleElement && titleElement.textContent.toLowerCase().includes(targetText)) {
            nextMatchIndex = i;
            break; // Stop searching once we find the *next* match
        }
    }

    // If a match is found, highlight it and scroll to it
    if (nextMatchIndex !== -1) {
        const matchElement = mediaElements[nextMatchIndex];
        matchElement.classList.add('highlighted-text');
        matchElement.scrollIntoView({ behavior: 'auto', block: 'center' });
        lastFoundIndex = nextMatchIndex;
        isSearching = false; // Stop searching after finding a match
    } else {
        // No match found in the currently visible elements.  Scroll down.
        lastScrollHeight = document.documentElement.scrollHeight;  // Store current height
        window.scrollTo({ top: lastScrollHeight, behavior: 'auto' });

        // Set a timeout to continue searching after a delay (to allow new content to load)
        searchTimeout = setTimeout(() => {
            if (!isSearching) return; // Check if searching was stopped

            // Check if we've reached the end of the page
            if (document.documentElement.scrollHeight === lastScrollHeight) {
                stopSearch();
                showNoResultsMessage(); // No more content, and no match found
            } else {
                 isSearching = false; // allow searchAndScroll to run it again
                searchAndScroll();    // Continue searching with newly loaded content
            }
        }, SCROLL_DELAY_MS);
    }

    // Set overall search timeout (for extreme cases)
     searchTimeout = setTimeout(() => {
       stopSearch();
       showErrorMessage("Search timed out.");
    }, SEARCH_TIMEOUT_MS);
}

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

})();