您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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.
当前为
// ==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 })();