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

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. Uses jQuery. Works in background tabs.

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

您需要先安装一个扩展,例如 篡改猴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) - jQuery & Background
// @match        https://www.youtube.com/*
// @grant        GM_addStyle
// @version      3.1
// @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. Uses jQuery.  Works in background tabs.
// @author       Your Name (with further optimization & jQuery)
// @license      MIT
// @namespace    https://greasyfork.org/users/1435316
// @require      https://code.jquery.com/jquery-3.7.1.min.js
// ==/UserScript==

(function() {
    'use strict';

    let targetText = "";
    let $searchBox;
    let isSearching = false;
    let $searchInput;
    let $searchButton;
    let $stopButton;
    let $prevButton;
    let $nextButton;
    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 (jQuery objects)
    let currentHighlightIndex = -1; // Index of *currently* highlighted element
    let lastScrollHeight = 0;
    let observer = null;  // MutationObserver
    let isPaused = false; // Flag to pause the observer


    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 = $('<div>', {
            id: 'floating-search-box',
            role: 'search'
        });

        $searchInput = $('<input>', {
            type: 'text',
            placeholder: 'Search to scroll...',
            value: '',
            'aria-label': 'Search within YouTube grid',
            maxlength: MAX_SEARCH_LENGTH
        });

        $searchButton = $('<button>', {
            text: 'Search',
            'aria-label': 'Start search',
            click: searchAndScroll
        });

        $prevButton = $('<button>', {
            text: 'Prev',
            id: 'prev-result-button',
            'aria-label': 'Previous result',
            disabled: true,
            click: () => navigateResults(-1)
        });

        $nextButton = $('<button>', {
            text: 'Next',
            id: 'next-result-button',
            'aria-label': 'Next result',
            disabled: true,
            click: () => navigateResults(1)
        });

        $stopButton = $('<button>', {
            text: 'Stop',
            id: 'stop-search-button',
            'aria-label': 'Stop search',
            click: stopSearch
        });

        $searchBox.append($searchInput, $searchButton, $prevButton, $nextButton, $stopButton);

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

        if ($mastheadEnd.length) {
            if ($buttonsContainer.length) {
                $searchBox.insertBefore($buttonsContainer);
            } else {
                $mastheadEnd.append($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.");
            $('body').prepend($searchBox); // Fallback
        }
    }


    // --- Show Error Message ---
    function showErrorMessage(message) {
        let $errorDiv = $('#search-error-message');
        if (!$errorDiv.length) {
            $errorDiv = $('<div>', { id: 'search-error-message' }).appendTo('body');
        }
        $errorDiv.text(message).show();

        setTimeout(() => $errorDiv.hide(), 5000);
    }

    // --- Show "No Results" Message ---
    function showNoResultsMessage() {
        let $noResultsDiv = $('#search-no-results-message');
        if (!$noResultsDiv.length) {
            $noResultsDiv = $('<div>', { id: 'search-no-results-message', text: "No matching results found." }).appendTo('body');
        }
        $noResultsDiv.show();

        setTimeout(() => $noResultsDiv.hide(), 5000);
    }


    // --- Stop Search Function ---
    function stopSearch() {
        isSearching = false;
        clearTimeout(searchTimeout);
        currentHighlightIndex = -1;
        highlightedElements.forEach($el => $el.removeClass('highlighted-text').css('position', ''));
        highlightedElements = []; // Clear the array
        updateNavButtons();
        resumeObserver();  // Resume when search stops
    }

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

        currentHighlightIndex += direction;

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

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

    // --- Update Navigation Buttons State ---
    function updateNavButtons() {
        $prevButton.prop('disabled', highlightedElements.length <= 1);
        $nextButton.prop('disabled', highlightedElements.length <= 1);
    }


    // --- Pause the observer ---
    function pauseObserver() {
        if (observer && !isPaused) {
            observer.disconnect();
            isPaused = true;
            //console.log("Observer paused");
        }
    }

    // --- Resume the observer ---
    function resumeObserver() {
        if (observer && isPaused) {
             observeDOM(); // Re-connect
            isPaused = false;
            //console.log("Observer resumed");
        }
    }


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

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

        pauseObserver(); // Pause the observer during active search

        // Get all media elements (regardless of visibility)
        const $mediaElements = $('ytd-rich-grid-media');  // No :visible check
        let foundMatch = false;

        highlightedElements = []; // Clear previous results
        $mediaElements.each(function() {
            const $titleElement = $(this).find('#video-title');
            if ($titleElement.length && $titleElement.text().toLowerCase().includes(targetText)) {
                $(this).addClass('highlighted-text');
                highlightedElements.push($(this));
                foundMatch = true;
            } else {
                $(this).removeClass('highlighted-text'); // Remove highlighting if no match
            }
        });


        // Find the next match index
        let nextMatchIndex = -1;
        if(currentHighlightIndex !== -1 && highlightedElements.length > 0) {
            // Start searching from the element *after* the current one in highlightedElements
            for(let i = currentHighlightIndex + 1; i < highlightedElements.length; i++) {
                if(highlightedElements[i].is(":visible")){ //Only consider visible ones for scrolling
                    nextMatchIndex = i;
                    break;
                }
            }

        } else {
             // If no previous match, find the *first* visible one
            for (let i = 0; i < highlightedElements.length; i++) {
                if (highlightedElements[i].is(":visible")) {
                    nextMatchIndex = i;
                    break;
                }
            }
        }


        if (nextMatchIndex !== -1) {
             // Scroll to the *next* match
            highlightedElements[nextMatchIndex][0].scrollIntoView({ behavior: 'auto', block: 'center' });
            currentHighlightIndex = nextMatchIndex;
            updateNavButtons();
            isSearching = false; // Stop searching after first visible match

        } else {
              // No new *visible* match found, scroll down
            lastScrollHeight = document.documentElement.scrollHeight;
             // Use native scrollTo for background tab compatibility
            window.scrollTo({ top: lastScrollHeight, behavior: 'auto' });

            searchTimeout = setTimeout(() => {
                if (!isSearching) return; // Check again, in case stopSearch was called

                 if (document.documentElement.scrollHeight === lastScrollHeight) { // End of page
                    stopSearch();
                    if (!foundMatch) {
                        showNoResultsMessage();
                    }
                } else {
                    isSearching = false;  // Allow re-entry
                    searchAndScroll();    // Continue searching after scroll
                }
            }, SCROLL_DELAY_MS);
        }

        searchTimeout = setTimeout(() => {
            stopSearch();
            showErrorMessage("Search timed out.");
        }, SEARCH_TIMEOUT_MS);
    }


    // --- Mutation Observer ---
    function observeDOM() {
        if (observer) {
            observer.disconnect(); // Disconnect any existing observer
        }

        observer = new MutationObserver(mutations => {
            if (!isSearching) { // Only re-run search if NOT actively searching
                searchAndScroll(); // Re-evaluate on DOM changes
            }
        });

        // Observe the entire document body for changes
        observer.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['style', 'class'] // Observe style and class changes
        });
    }



    // --- Initialization ---
    createSearchBox();
    observeDOM(); // Start observing


})();