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

Automatically scrolls to the *next* matching video title, highlighting all matches. Search box, search/stop, next/previous buttons, animated border, no results message. Handles dynamic loading correctly. Does NOT remember previous search.

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         YouTube Grid Auto-Scroll & Search (Ultra Optimized - Instant)
// @match        https://www.youtube.com/*
// @grant        GM_addStyle
// @version      3.0
// @description  Automatically scrolls to the *next* matching video title, highlighting all matches. Search box, search/stop, next/previous buttons, animated border, no results message. Handles dynamic loading correctly.  Does NOT remember previous search.
// @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 prevButton; // Previous button
    let nextButton; // Next button
    let searchTimeout;
    const SEARCH_TIMEOUT_MS = 20000; // 20 seconds
    const SCROLL_DELAY_MS = 750;
    const MAX_SEARCH_LENGTH = 255;
    let highlightedElements = []; // Array to store all highlighted elements
    let currentHighlightIndex = -1; // Index of *currently* highlighted element in highlightedElements
    let lastScrollHeight = 0;

    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;
        }

        /* Style for navigation buttons */
        #prev-result-button, #next-result-button {
            background-color: #444;
            color: white;
            margin: 0 3px; /* Add some spacing */
        }
        #prev-result-button:hover, #next-result-button:hover {
            background-color: #555;
        }

         .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; /* 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');

        searchInput = document.createElement('input');
        searchInput.type = 'text';
        searchInput.placeholder = 'Search to scroll...';
        // IMPORTANT:  Remove GM_getValue.  It will now start blank.
        searchInput.value = ''; // Start with an empty search box
        searchInput.setAttribute('aria-label', 'Search within YouTube grid');
        searchInput.maxLength = MAX_SEARCH_LENGTH;

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

        // Previous and Next buttons
        prevButton = document.createElement('button');
        prevButton.textContent = 'Prev';
        prevButton.id = 'prev-result-button';
        prevButton.addEventListener('click', () => navigateResults(-1)); // -1 for previous
        prevButton.setAttribute('aria-label', 'Previous result');
        prevButton.disabled = true; // Initially disabled

        nextButton = document.createElement('button');
        nextButton.textContent = 'Next';
        nextButton.id = 'next-result-button';
        nextButton.addEventListener('click', () => navigateResults(1)); // 1 for next
        nextButton.disabled = true; // Initially disabled

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

        searchBox.appendChild(searchInput);
        searchBox.appendChild(searchButton);
        searchBox.appendChild(prevButton);
        searchBox.appendChild(nextButton);
        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
        }
    }

    // --- 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() {
        isSearching = false;
        clearTimeout(searchTimeout);
        currentHighlightIndex = -1; // Reset current highlight index
        // Remove highlighting, but keep the array for potential re-use
        document.querySelectorAll('.highlighted-text').forEach(el => {
            el.classList.remove('highlighted-text');
            el.style.position = '';
        });
        updateNavButtons(); // Disable buttons
    }

    // --- Navigate Between Results ---
    function navigateResults(direction) {
        if (highlightedElements.length === 0) return;

        currentHighlightIndex += direction;

        // Wrap around
        if (currentHighlightIndex < 0) {
            currentHighlightIndex = highlightedElements.length - 1;
        } else if (currentHighlightIndex >= highlightedElements.length) {
            currentHighlightIndex = 0;
        }

        highlightedElements[currentHighlightIndex].scrollIntoView({ behavior: 'auto', block: 'center' });
        updateNavButtons();
    }

    // --- Update Navigation Buttons State ---
    function updateNavButtons() {
        prevButton.disabled = highlightedElements.length <= 1;
        nextButton.disabled = highlightedElements.length <= 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;
        }

        // IMPORTANT: Remove GM_setValue.  Don't save the search term.

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

        // Find *all* matching elements and add them to highlightedElements
        highlightedElements = []; // Clear previous results
        for (let i = 0; i < mediaElements.length; i++) {
            const titleElement = mediaElements[i].querySelector('#video-title');
            if (titleElement && titleElement.textContent.toLowerCase().includes(targetText)) {
                mediaElements[i].classList.add('highlighted-text');
                highlightedElements.push(mediaElements[i]);
                foundMatch = true;
            }
        }

        // Find the next match index, starting from the *element after* the current one
        let nextMatchIndex = -1;
        if (currentHighlightIndex !== -1 && highlightedElements.length > 0) {
          // Find the DOM element corresponding to currentHighlightIndex
          let currentElement = highlightedElements[currentHighlightIndex];

          // Get all currently visible media elements *again* (because more might have loaded)
          const currentMediaElements = Array.from(document.querySelectorAll('ytd-rich-grid-media:not([style*="display: none"])'));

          // Find the index of the current element within the *current* DOM
          let currentDomIndex = currentMediaElements.indexOf(currentElement);

          // Start searching from the element *after* the current one in the DOM
          for (let i = currentDomIndex + 1; i < currentMediaElements.length; i++) {
              const titleElement = currentMediaElements[i].querySelector('#video-title');
              if (titleElement && titleElement.textContent.toLowerCase().includes(targetText)) {
                  // Find the index of this element within highlightedElements
                  nextMatchIndex = highlightedElements.indexOf(currentMediaElements[i]);
                  break;
              }
          }
      } else {
          // If no previous match, find the first one
          if (highlightedElements.length > 0) {
              nextMatchIndex = 0;
          }
      }


        if (nextMatchIndex !== -1) {
            // Scroll to the next match
            highlightedElements[nextMatchIndex].scrollIntoView({ behavior: 'auto', block: 'center' });
            currentHighlightIndex = nextMatchIndex;
            updateNavButtons();
            isSearching = false; // Stop searching after finding a match
        } else {
            // No new match found in the currently visible elements, scroll down
            lastScrollHeight = document.documentElement.scrollHeight;
            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 we've reached the end of the page
                if (document.documentElement.scrollHeight === lastScrollHeight) {
                    stopSearch();
                    if (!foundMatch) {
                        showNoResultsMessage();
                    }
                } else {
                    isSearching = false; // Allow re-entry into searchAndScroll
                    searchAndScroll(); // Continue searching
                }
            }, SCROLL_DELAY_MS);
        }

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

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

})();