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

Automatically scrolls to matching video titles on YouTube, highlighting all matches. Search box, search/stop, next/previous buttons, animated border, no results message. Uses jQuery. Works in background tabs. Only searches when the Search button is clicked.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         YouTube Grid Auto-Scroll & Search (Ultra Optimized - Instant) - jQuery & Background - On Demand
// @match        https://www.youtube.com/*
// @grant        GM_addStyle
// @version      3.2
// @description  Automatically scrolls to matching video titles on YouTube, highlighting all matches.  Search box, search/stop, next/previous buttons, animated border, no results message. Uses jQuery.  Works in background tabs.  Only searches when the Search button is clicked.
// @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 = []; // jQuery objects
    let currentHighlightIndex = -1;
    let lastScrollHeight = 0;
    let observer = null;
    let isPaused = false; // Flag to pause the observer
    let initialSearch = true; // Flag to run search function on first click


      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) $buttonsContainer.length ? $searchBox.insertBefore($buttonsContainer) : $mastheadEnd.append($searchBox);
        else { console.error("Could not find YouTube masthead end."); showErrorMessage("Masthead not found. Search box at top."); $('body').prepend($searchBox); }
    }

    // --- Show Error/No Results Messages ---
    function showMessage(message, type) {
        const id = type === 'error' ? 'search-error-message' : 'search-no-results-message';
        let $messageDiv = $('#' + id);
        if (!$messageDiv.length) $messageDiv = $('<div>', { id: id }).appendTo('body');
        $messageDiv.text(message).show();
        setTimeout(() => $messageDiv.hide(), 5000);
    }
    const showErrorMessage = (message) => showMessage(message, 'error');
    const showNoResultsMessage = () => showMessage("No matching results found.", 'no-results');


    // --- Stop Search ---
    function stopSearch() {
        isSearching = false;
        initialSearch = true; // Reset initial search flag
        clearTimeout(searchTimeout);
        currentHighlightIndex = -1;
        highlightedElements.forEach($el => $el.removeClass('highlighted-text').css('position', ''));
        highlightedElements = [];
        updateNavButtons();
        resumeObserver();
    }

    // --- Navigate Results ---
    function navigateResults(direction) {
        if (highlightedElements.length === 0) return;
        currentHighlightIndex = (currentHighlightIndex + direction + highlightedElements.length) % highlightedElements.length;
        highlightedElements[currentHighlightIndex][0].scrollIntoView({ behavior: 'auto', block: 'center' });
        updateNavButtons();
    }

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

    // --- Pause/Resume Observer ---
    function pauseObserver() {
        if (observer && !isPaused) { observer.disconnect(); isPaused = true; }
    }
    function resumeObserver() {
        if (observer && isPaused) { observeDOM(); isPaused = false; }
    }

    // --- Search and Scroll ---
    function searchAndScroll() {
        if (isSearching && !initialSearch) return; // Prevent re-entry if already searching (and not the initial search)
        isSearching = true;
        initialSearch = false; // Set the flag to false after the first click
        clearTimeout(searchTimeout);

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

        pauseObserver();
        const $mediaElements = $('ytd-rich-grid-media');
        let foundMatch = false;

        highlightedElements = [];
        $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');
        });

        let nextMatchIndex = -1;
        if (currentHighlightIndex !== -1 && highlightedElements.length > 0) {
            for (let i = currentHighlightIndex + 1; i < highlightedElements.length; i++)
                if (highlightedElements[i].is(":visible")) { nextMatchIndex = i; break; }
        } else {
            for (let i = 0; i < highlightedElements.length; i++)
                if (highlightedElements[i].is(":visible")) { nextMatchIndex = i; break; }
        }

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

        } else {
            lastScrollHeight = document.documentElement.scrollHeight;
            window.scrollTo({ top: lastScrollHeight, behavior: 'auto' });
            searchTimeout = setTimeout(() => {
                if (!isSearching) return;
                if (document.documentElement.scrollHeight === lastScrollHeight) {
                    stopSearch();
                    if (!foundMatch) showNoResultsMessage();
                } else {
                    isSearching = false; // Allow re-entry after scroll
                    searchAndScroll();
                }
            }, SCROLL_DELAY_MS);
        }

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

    // --- Mutation Observer ---
    function observeDOM() {
        if (observer) observer.disconnect();
        observer = new MutationObserver(() => {
              //No action if the user did not click on "search"
        });
        observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });
    }

    // --- Initialization ---
    createSearchBox();
    observeDOM(); // Start observing, but it won't do anything until the search button is clicked

})();