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

您需要先安裝使用者腳本管理器擴展,如 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) - 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


})();