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 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();

})();