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

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

})();