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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();

})();